From 056d399b6bb1a2adcc1f926ce837842f80e31109 Mon Sep 17 00:00:00 2001 From: Krish Katariya Date: Sun, 27 Jul 2025 17:04:16 -0700 Subject: [PATCH 01/17] Setup basic v2 of API with compute and storage resource capabilities --- .../.gradle/8.14.3/checksums/checksums.lock | Bin 0 -> 17 bytes .../8.14.3/checksums/md5-checksums.bin | Bin 0 -> 33747 bytes .../8.14.3/checksums/sha1-checksums.bin | Bin 0 -> 83831 bytes .../executionHistory/executionHistory.bin | Bin 0 -> 256724 bytes .../executionHistory/executionHistory.lock | Bin 0 -> 17 bytes .../.gradle/8.14.3/fileChanges/last-build.bin | Bin 0 -> 1 bytes .../.gradle/8.14.3/fileHashes/fileHashes.bin | Bin 0 -> 22397 bytes .../.gradle/8.14.3/fileHashes/fileHashes.lock | Bin 0 -> 17 bytes .../8.14.3/fileHashes/resourceHashesCache.bin | Bin 0 -> 20979 bytes .../.gradle/8.14.3/gc.properties | 0 .../buildOutputCleanup.lock | Bin 0 -> 17 bytes .../buildOutputCleanup/cache.properties | 2 + .../buildOutputCleanup/outputFiles.bin | Bin 0 -> 18947 bytes .../.gradle/file-system.probe | Bin 0 -> 8 bytes .../.gradle/vcs-1/gc.properties | 0 modules/admin-api-server/HELP.md | 31 ++ .../research-service/pom.xml | 6 + .../research/service/config/WebMvcConfig.java | 5 +- .../service/v2/config/V2DataInitializer.java | 320 ++++++++++++++++++ .../controller/ComputeResourceController.java | 251 ++++++++++++++ .../controller/StorageResourceController.java | 254 ++++++++++++++ .../service/v2/entity/ComputeResource.java | 253 ++++++++++++++ .../service/v2/entity/StorageResource.java | 263 ++++++++++++++ .../repository/ComputeResourceRepository.java | 52 +++ .../repository/StorageResourceRepository.java | 52 +++ .../src/main/resources/application.yml | 17 +- 26 files changed, 1498 insertions(+), 8 deletions(-) create mode 100644 modules/admin-api-server/.gradle/8.14.3/checksums/checksums.lock create mode 100644 modules/admin-api-server/.gradle/8.14.3/checksums/md5-checksums.bin create mode 100644 modules/admin-api-server/.gradle/8.14.3/checksums/sha1-checksums.bin create mode 100644 modules/admin-api-server/.gradle/8.14.3/executionHistory/executionHistory.bin create mode 100644 modules/admin-api-server/.gradle/8.14.3/executionHistory/executionHistory.lock create mode 100644 modules/admin-api-server/.gradle/8.14.3/fileChanges/last-build.bin create mode 100644 modules/admin-api-server/.gradle/8.14.3/fileHashes/fileHashes.bin create mode 100644 modules/admin-api-server/.gradle/8.14.3/fileHashes/fileHashes.lock create mode 100644 modules/admin-api-server/.gradle/8.14.3/fileHashes/resourceHashesCache.bin create mode 100644 modules/admin-api-server/.gradle/8.14.3/gc.properties create mode 100644 modules/admin-api-server/.gradle/buildOutputCleanup/buildOutputCleanup.lock create mode 100644 modules/admin-api-server/.gradle/buildOutputCleanup/cache.properties create mode 100644 modules/admin-api-server/.gradle/buildOutputCleanup/outputFiles.bin create mode 100644 modules/admin-api-server/.gradle/file-system.probe create mode 100644 modules/admin-api-server/.gradle/vcs-1/gc.properties create mode 100644 modules/admin-api-server/HELP.md create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/StorageResourceRepository.java diff --git a/modules/admin-api-server/.gradle/8.14.3/checksums/checksums.lock b/modules/admin-api-server/.gradle/8.14.3/checksums/checksums.lock new file mode 100644 index 0000000000000000000000000000000000000000..91b779c3a9fb78cabe5b374c64cd88f7a169d91a GIT binary patch literal 17 VcmZQpa{N1q_hmsB0~oNF0st?G1RnqZ literal 0 HcmV?d00001 diff --git a/modules/admin-api-server/.gradle/8.14.3/checksums/md5-checksums.bin b/modules/admin-api-server/.gradle/8.14.3/checksums/md5-checksums.bin new file mode 100644 index 0000000000000000000000000000000000000000..660ea1094523704f8855c53df58e01e033100820 GIT binary patch literal 33747 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 literal 0 HcmV?d00001 diff --git a/modules/admin-api-server/.gradle/8.14.3/executionHistory/executionHistory.lock b/modules/admin-api-server/.gradle/8.14.3/executionHistory/executionHistory.lock new file mode 100644 index 0000000000000000000000000000000000000000..eecb9426f9f4b6f258972e09d3d7229f84840af9 GIT binary patch literal 17 UcmZQ}zyHu^|Ex{i3=m)r05`n^iU0rr literal 0 HcmV?d00001 diff --git a/modules/admin-api-server/.gradle/8.14.3/fileChanges/last-build.bin b/modules/admin-api-server/.gradle/8.14.3/fileChanges/last-build.bin new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/modules/admin-api-server/.gradle/8.14.3/fileHashes/fileHashes.bin b/modules/admin-api-server/.gradle/8.14.3/fileHashes/fileHashes.bin new file mode 100644 index 0000000000000000000000000000000000000000..2164ecb07c1fb6e4a6cf6aebd94778f8e0b0457c GIT binary patch literal 22397 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 literal 0 HcmV?d00001 diff --git a/modules/admin-api-server/.gradle/8.14.3/fileHashes/fileHashes.lock b/modules/admin-api-server/.gradle/8.14.3/fileHashes/fileHashes.lock new file mode 100644 index 0000000000000000000000000000000000000000..c705cfc2b9028bf7c288383ea7f9254af896e7fe GIT binary patch literal 17 VcmZQxc7yf3-CDmA1~6bW0RS~(1Xut7 literal 0 HcmV?d00001 diff --git a/modules/admin-api-server/.gradle/8.14.3/fileHashes/resourceHashesCache.bin b/modules/admin-api-server/.gradle/8.14.3/fileHashes/resourceHashesCache.bin new file mode 100644 index 0000000000000000000000000000000000000000..f7c24a8b7dca851949b3a5290453bf9ef06ca09b GIT binary patch literal 20979 zcmeI3i8oYj1HeV7ktItiTWFye87WJ)5W`qTdHWQ}UY5{EyzwfO4^dglmR+I6ZeEgi zyjen)LZKANT1`@;Eb%qx-p6}Bcm9I+Jaf))&(H5X&%M99%$(z112(pe0!!$N?Vp#( zKZ|e)7Jvm{0aySQfCXRySO6A)1z-VK02Y7+U;$VF7Jvm{0aySQfCc_71+0H<2oBkp zjSB3{1^#nvY`fR}S|DDtedwbZm_k7QOq>V*Ke)b?pja2Aa|Pj~9KaK5lQ)rUR%s&K zLLTs>&m{{PT68yrn_UMy<74HNxPG22I({PHc>>{1Q6E#A5pG%mcz)z!=%J1cLkPE2 z2fVQHj?O8WLK?y?x&Xf)D4t%F9@dHa!^=FlT~OP^E(-Oz0WYpB<&pJKL30qvfR`N8 zc0V2#6O8!A&CA@Yz=Lrv3XgD-J>X?MCgil4<6;OmB-0UpIz2KSLAZq};Jm#r@M`il5~%MDIA7HJuXg62(DR*00epS!;%B{( zjC{l=H2}W3R6%VZv#uNA(^c)gyO994dDHKz!49z}3{$5{^Wi zs6e?d;QDuSePuMRZ$-HA2f&TSn}!IQ6DkNdGXvb1Z_Nc6y+B2To9F<3q+B#YL5>GK zFUXvL+cXF1QxgXr(RpV9x20{CsB1P^LdWj`+OeDfDqA%v5O%UsQT_lLaG^$52- zw#?gv)pQwJv8Z1JxTBiS*~0I(=y`3y0Nh38rc*=rXdXIF#xkEN&V44LxEtX_UclYX zEOp{3=fx3D8Ux%NXM5GW&GIe6Ev^FY(cf}Sa$5yI!p*Y)_jXM(3FoU9M!2OY;6A14 zd#!R#j3eCaJK$$_59rH;yP?OMNz*bnt+%{(etrh=O(X#K^OW{B)0veI+W`)Kv$1ha zAY;LwumCIo3%~-f04x9tzyh!UEC36@0rt&>%E@&7RY|jogk!gnf&SAmcg-~=emAD z$^SP(wj>EY<-XJ@+%YG6CRJh(Yv|NkZFfj%dY658^9_PC&kn4?+2e7qC3b$`+ivR` zvQLvc)+l&zKys%_d5;4{mLc`WfDYE+plsw#KYTb~vQ1Y^G9)P+YYg8C=ocIIJmmNK z>6%HN?k8Bo)LA)BSRkXDw8%GZ% zj0j1zOpAqt`;%8;4F~raulhKP;r=fl3#|N)b7KuTw#d~(bJE@5*F>EkFwRg9;U=80@d~LcIHW z{XEqlC2wlam_3qv3_Awd-@dnR@@SfWsp0@1lUy+{eZFMkrnA}3jaY*g zS9O4qaT8~lK`P|hpwWRfLgUKC5;OhtC_Z1ON;h&*utxVG{r@(3HI+D6r>}|N5%0ws zOF!}1-(Oj3T{u0G)%L8HdH?^4eLJsLojSC{@zt1~)WNRW@;lZ@R*r5P8jEtIsn5O? z3a(>um8!xZA@?ip-+o)vU&g!vTAidneq2|C`Lfd}?+lTOyST@Sz-eXBOBKfU@R z<#0~seT_gJ<{kYj#)#SaqE-Hf<#Xr7#FqRAhgtNvBs8Est@<{sNF@Kj_dUBpuwz&X z#^{F0Pp~a=wD|r}7`qE=a8^x{Q-&0WrcQ576n;rNf;IZ$1>fB=x1h}2o9)SD*N?;+ z(!|`Z=%@Jjnhg%c{UWj~nx;wiihn3pdNXHVEK&1F>L+%L8?_EUvnZKYPMs)e>U6WR z#u}B+RgK1-qF;!-xNA7f)4+UYXhku0C99uB3!$EEWIk=DlQzPV3pA}ir!=EY|MGxOjJJ{uCNEBPhz#63-MtpWYj5&fW<4?j~1_#!N%+uf) zmkq31FW9)1BVvNZ8Py|^++IOGp}MPQTxBWTn#JD#Pt1rCLG%@Gcv(wKIb)8+_4Ha) zC)G(0M^h4(U88KX$b4>U#kSZAFZ2z+xvFg>o_)h`UdIP(jKs&-tmTYvOq4nDBL_Eo z5^IcK(m1L-rRhSA{#M#6O=a=v(Nj9^I(SH!V0|O^QMEi5i|cx3nd8Sn2hO%`RTr-M#c_BYS?@Q8}!G6O-Kr>UCRFKP|=|#1{L8Y z$czSAbO?(mEQcQK@D8;QCqYOS4aN*Yp`;+F*Y^7KdwD1Rpv`8P9=Rq;yJi19v;BH{UF$^sD%q!Jws#C}3FKLyWxidzr!*4SX!(;epR3*L zU0e8e{N^c{4{I;3d0sM55RJ+FdhJ$@Z+pDx+8603weNp?B~;rKAC-MRYCrHJfA8X! zXL3&eL+zoJuHdT=cZOsikM4_s$1u$k6)1aZQAwSnCzpO&v-=u z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** l5STB4&CW)7&YdN + + com.h2database + h2 + runtime + org.springframework.boot spring-boot-starter-web @@ -229,6 +234,7 @@ under the License. maven-compiler-plugin 17 + true diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/WebMvcConfig.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/WebMvcConfig.java index 06b393c56a4..9959b341c8e 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/WebMvcConfig.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/WebMvcConfig.java @@ -36,8 +36,9 @@ public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins(deployedOrigin, devOrigin) + .allowedOrigins(deployedOrigin, devOrigin, "http://localhost:5173", "http://localhost:3000") .allowedMethods("GET", "POST", "OPTIONS", "PATCH", "DELETE", "PUT") - .allowedHeaders("*"); + .allowedHeaders("*") + .allowCredentials(true); } } diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java new file mode 100644 index 00000000000..d61044f28c8 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java @@ -0,0 +1,320 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.config; + +import jakarta.annotation.PostConstruct; +import java.util.List; +import org.apache.airavata.research.service.v2.entity.ComputeResource; +import org.apache.airavata.research.service.v2.entity.StorageResource; +import org.apache.airavata.research.service.v2.repository.ComputeResourceRepository; +import org.apache.airavata.research.service.v2.repository.StorageResourceRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class V2DataInitializer { + + private static final Logger LOGGER = LoggerFactory.getLogger(V2DataInitializer.class); + + private final ComputeResourceRepository computeResourceRepository; + private final StorageResourceRepository storageResourceRepository; + + public V2DataInitializer(ComputeResourceRepository computeResourceRepository, + StorageResourceRepository storageResourceRepository) { + this.computeResourceRepository = computeResourceRepository; + this.storageResourceRepository = storageResourceRepository; + } + + @PostConstruct + public void initializeData() { + LOGGER.info("Initializing V2 mock data for compute and storage resources..."); + + initializeComputeResources(); + initializeStorageResources(); + + LOGGER.info("V2 mock data initialization completed."); + } + + private void initializeComputeResources() { + if (computeResourceRepository.count() == 0) { + LOGGER.info("Creating mock compute resources..."); + + List computeResources = List.of( + new ComputeResource( + "Bridges-2 Supercomputer", + "Advanced high-performance computing cluster at Pittsburgh Supercomputing Center with GPU acceleration for AI workloads.", + "bridges2.psc.edu", + "HPC", + 1280, + 2560, + "CentOS 7", + "SLURM", + "Features GPU nodes for machine learning, regular memory and extreme memory configurations. Maximum job time: 48 hours.", + "Pittsburgh Supercomputing Center" + ), + + new ComputeResource( + "Expanse Supercomputer", + "SDSC's newest supercomputer designed for scientific computing with specialized GPU nodes for machine learning and AI research.", + "expanse.sdsc.edu", + "HPC", + 1408, + 2816, + "CentOS 8", + "SLURM", + "CPU and GPU partitions available. Optimized for parallel computing and machine learning workloads.", + "San Diego Supercomputer Center" + ), + + new ComputeResource( + "Anvil Cluster", + "Purdue University's advanced computing cluster with high-memory nodes and specialized hardware for data analytics.", + "anvil.rcac.purdue.edu", + "HPC", + 1000, + 4000, + "Red Hat Enterprise Linux 8", + "SLURM", + "High-memory nodes (1.5TB RAM), GPU nodes with V100 and A100 cards for deep learning applications.", + "Purdue University RCAC" + ), + + new ComputeResource( + "Frontera Supercomputer", + "Leadership-class supercomputer at TACC for large-scale computational research and simulation.", + "frontera.tacc.utexas.edu", + "HPC", + 8008, + 16000, + "CentOS 7", + "SLURM", + "Leadership computing facility with specialized queues for different workload types. Maximum allocation: 3M core-hours.", + "Texas Advanced Computing Center" + ), + + new ComputeResource( + "AWS EC2 Compute Cloud", + "Scalable cloud computing platform with on-demand instance provisioning and auto-scaling capabilities.", + "amazonaws.com", + "Cloud", + 9999, + 99999, + "Amazon Linux 2", + "Cloud Native", + "Pay-as-you-go pricing model with various instance types (CPU, memory, GPU optimized). Global availability zones.", + "Amazon Web Services" + ), + + new ComputeResource( + "Google Cloud Compute Engine", + "High-performance virtual machines with custom machine types and specialized accelerators for AI/ML workloads.", + "compute.googleapis.com", + "Cloud", + 8888, + 88888, + "Ubuntu 20.04 LTS", + "Cloud Native", + "Preemptible instances available for cost savings. TPUs available for machine learning acceleration.", + "Google Cloud Platform" + ), + + new ComputeResource( + "XSEDE Comet", + "GPU-accelerated supercomputer optimized for data-intensive computing and machine learning applications.", + "comet.sdsc.edu", + "HPC", + 1980, + 2640, + "CentOS 7", + "SLURM", + "72 GPU nodes with K80 cards, high-speed interconnect, and parallel file systems for data-intensive research.", + "San Diego Supercomputer Center" + ), + + new ComputeResource( + "Jetstream2 Cloud", + "National cyberinfrastructure providing on-demand virtual machines for academic research computing.", + "jetstream-cloud.org", + "Cloud", + 2000, + 8000, + "Various Linux Distributions", + "OpenStack", + "Self-service cloud environment with support for containers, Kubernetes, and Jupyter notebooks.", + "Indiana University & TACC" + ), + + new ComputeResource( + "NERSC Perlmutter", + "Exascale-class supercomputer with GPU acceleration designed for scientific computing and AI convergence.", + "perlmutter.nersc.gov", + "HPC", + 6159, + 4915, + "SUSE Linux Enterprise", + "SLURM", + "A100 GPU nodes optimized for mixed-precision computing. Advanced interconnect and parallel file systems.", + "National Energy Research Scientific Computing Center" + ) + ); + + computeResourceRepository.saveAll(computeResources); + LOGGER.info("Created {} compute resources", computeResources.size()); + } + } + + private void initializeStorageResources() { + if (storageResourceRepository.count() == 0) { + LOGGER.info("Creating mock storage resources..."); + + List storageResources = List.of( + new StorageResource( + "TACC Ranch Archive", + "Petascale archival storage system for long-term data preservation and backup with tape-based architecture.", + "ranch.tacc.utexas.edu", + "Archive Storage", + 20000L, + "SFTP", + "ranch.tacc.utexas.edu:22", + true, + false, + "Hierarchical storage management with automatic data migration. Designed for long-term archival.", + "Texas Advanced Computing Center" + ), + + new StorageResource( + "XSEDE Globus Data Transfer", + "High-performance data transfer and sharing service connecting research institutions worldwide.", + "globus.org", + "Data Transfer", + 50000L, + "GridFTP", + "https://transfer.api.globusonline.org", + true, + true, + "Parallel data transfer with resumption capabilities. Supports endpoint-to-endpoint transfers.", + "University of Chicago & XSEDE" + ), + + new StorageResource( + "AWS S3 Object Storage", + "Highly scalable object storage service with 99.999999999% durability and global accessibility.", + "s3.amazonaws.com", + "Object Storage", + 999999L, + "S3 API", + "https://s3.amazonaws.com", + true, + true, + "Multiple storage classes (Standard, IA, Glacier) with lifecycle policies for automatic cost optimization.", + "Amazon Web Services" + ), + + new StorageResource( + "Google Cloud Storage", + "Unified object storage platform with multi-regional availability and integrated machine learning capabilities.", + "storage.googleapis.com", + "Object Storage", + 888888L, + "S3 Compatible API", + "https://storage.googleapis.com", + true, + true, + "Integrated with BigQuery and AI Platform. Supports both hot and cold storage tiers.", + "Google Cloud Platform" + ), + + new StorageResource( + "NERSC HPSS Archive", + "High Performance Storage System providing long-term archival storage for scientific data.", + "archive.nersc.gov", + "Archive Storage", + 30000L, + "HPSS", + "hpss://archive.nersc.gov", + true, + false, + "Hierarchical storage management with robotic tape library. Optimized for large scientific datasets.", + "National Energy Research Scientific Computing Center" + ), + + new StorageResource( + "Open Science Data Federation", + "Distributed data federation providing access to scientific datasets across multiple institutions.", + "osdf.osg-htc.org", + "Distributed Storage", + 15000L, + "HTTP/HTTPS", + "https://osdf.osg-htc.org", + true, + false, + "Caching infrastructure for efficient data distribution. Supports both public and authenticated access.", + "Open Science Grid" + ), + + new StorageResource( + "SDSC Data Oasis", + "High-performance parallel file system designed for data-intensive computing and analytics workflows.", + "oasis.sdsc.edu", + "Parallel File System", + 12000L, + "NFS/Lustre", + "/oasis/projects", + false, + false, + "Lustre-based parallel file system with high IOPS capability. Optimized for concurrent access patterns.", + "San Diego Supercomputer Center" + ), + + new StorageResource( + "CyVerse Data Store", + "Comprehensive data management platform for life sciences research with integrated analysis tools.", + "datastore.cyverse.org", + "Research Data Platform", + 25000L, + "iRODS", + "https://data.cyverse.org", + true, + true, + "Metadata management, data sharing, and integrated analysis workflows. Specialized for life sciences.", + "University of Arizona CyVerse" + ), + + new StorageResource( + "HDF5 Cloud Storage", + "Cloud-optimized storage service designed specifically for HDF5 datasets and scientific data formats.", + "hdf5.cloud", + "Scientific Data Storage", + 5000L, + "REST API", + "https://api.hdf5.cloud", + true, + true, + "Native support for HDF5 datasets with cloud-optimized access patterns and metadata indexing.", + "HDF Group" + ) + ); + + storageResourceRepository.saveAll(storageResources); + LOGGER.info("Created {} storage resources", storageResources.size()); + } + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java new file mode 100644 index 00000000000..1877a28851b --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java @@ -0,0 +1,251 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import java.util.Optional; +import jakarta.validation.Valid; +import org.apache.airavata.research.service.v2.entity.ComputeResource; +import org.apache.airavata.research.service.v2.repository.ComputeResourceRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v2/rf/compute-resources") +@Tag(name = "Compute Resources V2", description = "V2 API for managing compute infrastructure resources") +public class ComputeResourceController { + + private static final Logger LOGGER = LoggerFactory.getLogger(ComputeResourceController.class); + + private final ComputeResourceRepository computeResourceRepository; + + public ComputeResourceController(ComputeResourceRepository computeResourceRepository) { + this.computeResourceRepository = computeResourceRepository; + } + + @Operation(summary = "Get all public compute resources with pagination") + @GetMapping("/public") + public ResponseEntity> getComputeResources( + @RequestParam(value = "pageNumber", defaultValue = "0") int pageNumber, + @RequestParam(value = "pageSize", defaultValue = "10") int pageSize, + @RequestParam(value = "nameSearch", required = false) String nameSearch, + @RequestParam(value = "tag", required = false) String[] tags) { + + LOGGER.info("Getting compute resources - page: {}, size: {}, search: {}", pageNumber, pageSize, nameSearch); + + Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending()); + Page resources; + + if (nameSearch != null && !nameSearch.trim().isEmpty()) { + resources = computeResourceRepository.findByNameSearchAndIsPublicTrueAndIsActiveTrue(nameSearch, pageable); + } else { + resources = computeResourceRepository.findByIsPublicTrueAndIsActiveTrue(pageable); + } + + LOGGER.info("Found {} compute resources", resources.getTotalElements()); + return ResponseEntity.ok(resources); + } + + @Operation(summary = "Get compute resource by ID") + @GetMapping("/public/{id}") + public ResponseEntity getComputeResourceById(@PathVariable("id") String id) { + LOGGER.info("Getting compute resource by ID: {}", id); + + Optional resource = computeResourceRepository.findById(id); + if (resource.isPresent()) { + return ResponseEntity.ok(resource.get()); + } else { + LOGGER.warn("Compute resource not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + } + + @Operation(summary = "Create new compute resource") + @PostMapping("/") + public ResponseEntity createComputeResource(@Valid @RequestBody ComputeResource computeResource, BindingResult bindingResult) { + LOGGER.info("Creating new compute resource: {}", computeResource.getName()); + + // Validation error handling + if (bindingResult.hasErrors()) { + String errorMessage = bindingResult.getFieldErrors().stream() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .reduce((msg1, msg2) -> msg1 + ", " + msg2) + .orElse("Validation failed"); + LOGGER.error("Validation errors: {}", errorMessage); + return ResponseEntity.badRequest().body("Validation failed: " + errorMessage); + } + + try { + // Set default values for fields that might be null + if (computeResource.getCpuCores() == null) { + computeResource.setCpuCores(1); // Default to 1 core + } + if (computeResource.getMemoryGB() == null) { + computeResource.setMemoryGB(1); // Default to 1 GB + } + if (computeResource.getIsPublic() == null) { + computeResource.setIsPublic(true); + } + if (computeResource.getIsActive() == null) { + computeResource.setIsActive(true); + } + + ComputeResource savedResource = computeResourceRepository.save(computeResource); + LOGGER.info("Created compute resource with ID: {}", savedResource.getId()); + + return ResponseEntity.status(HttpStatus.CREATED).body(savedResource); + } catch (Exception e) { + LOGGER.error("Error creating compute resource: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error creating compute resource: " + e.getMessage()); + } + } + + @Operation(summary = "Update compute resource") + @PutMapping("/{id}") + public ResponseEntity updateComputeResource(@PathVariable("id") String id, @Valid @RequestBody ComputeResource computeResource, BindingResult bindingResult) { + LOGGER.info("Updating compute resource with ID: {}", id); + + // Validation error handling + if (bindingResult.hasErrors()) { + String errorMessage = bindingResult.getFieldErrors().stream() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .reduce((msg1, msg2) -> msg1 + ", " + msg2) + .orElse("Validation failed"); + LOGGER.error("Validation errors: {}", errorMessage); + return ResponseEntity.badRequest().body("Validation failed: " + errorMessage); + } + + try { + Optional existingResource = computeResourceRepository.findById(id); + if (!existingResource.isPresent()) { + LOGGER.warn("Compute resource not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + + // Set the ID to ensure we update the correct resource + computeResource.setId(id); + + // Preserve creation timestamp + computeResource.setCreatedAt(existingResource.get().getCreatedAt()); + + ComputeResource updatedResource = computeResourceRepository.save(computeResource); + LOGGER.info("Successfully updated compute resource with ID: {}", id); + + return ResponseEntity.ok(updatedResource); + } catch (Exception e) { + LOGGER.error("Error updating compute resource with ID: {}", id, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error updating compute resource: " + e.getMessage()); + } + } + + @Operation(summary = "Delete compute resource") + @DeleteMapping("/{id}") + public ResponseEntity deleteComputeResource(@PathVariable("id") String id) { + LOGGER.info("Deleting compute resource with ID: {}", id); + + try { + Optional existingResource = computeResourceRepository.findById(id); + if (!existingResource.isPresent()) { + LOGGER.warn("Compute resource not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + + computeResourceRepository.deleteById(id); + LOGGER.info("Successfully deleted compute resource with ID: {}", id); + return ResponseEntity.ok().body("Compute resource deleted successfully"); + } catch (Exception e) { + LOGGER.error("Error deleting compute resource with ID: {}", id, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error deleting compute resource: " + e.getMessage()); + } + } + + @Operation(summary = "Search compute resources by keyword") + @GetMapping("/search") + public ResponseEntity> searchComputeResources( + @RequestParam(value = "keyword") String keyword) { + + LOGGER.info("Searching compute resources with keyword: {}", keyword); + + List resources = computeResourceRepository + .findByNameContainingIgnoreCaseAndIsPublicTrue(keyword); + + LOGGER.info("Found {} compute resources matching keyword: {}", resources.size(), keyword); + return ResponseEntity.ok(resources); + } + + @Operation(summary = "Get compute resources by type") + @GetMapping("/type/{computeType}") + public ResponseEntity> getComputeResourcesByType( + @PathVariable("computeType") String computeType) { + + LOGGER.info("Getting compute resources by type: {}", computeType); + + List resources = computeResourceRepository + .findByComputeTypeAndIsPublicTrue(computeType); + + LOGGER.info("Found {} compute resources of type: {}", resources.size(), computeType); + return ResponseEntity.ok(resources); + } + + @Operation(summary = "Star/unstar a compute resource") + @PostMapping("/{id}/star") + public ResponseEntity starComputeResource(@PathVariable("id") String id) { + LOGGER.info("Starring compute resource with ID: {}", id); + // For now, just return true - starring functionality can be implemented later + return ResponseEntity.ok(true); + } + + @Operation(summary = "Check if user starred a compute resource") + @GetMapping("/{id}/star") + public ResponseEntity checkComputeResourceStarred(@PathVariable("id") String id) { + LOGGER.info("Checking if compute resource is starred: {}", id); + // For now, just return false - starring functionality can be implemented later + return ResponseEntity.ok(false); + } + + @Operation(summary = "Get compute resource star count") + @GetMapping("/{id}/stars/count") + public ResponseEntity getComputeResourceStarCount(@PathVariable("id") String id) { + LOGGER.info("Getting star count for compute resource: {}", id); + // For now, just return 0 - starring functionality can be implemented later + return ResponseEntity.ok(0L); + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java new file mode 100644 index 00000000000..06b4b88d9f0 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java @@ -0,0 +1,254 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import java.util.Optional; +import jakarta.validation.Valid; +import org.apache.airavata.research.service.v2.entity.StorageResource; +import org.apache.airavata.research.service.v2.repository.StorageResourceRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v2/rf/storage-resources") +@Tag(name = "Storage Resources V2", description = "V2 API for managing storage infrastructure resources") +public class StorageResourceController { + + private static final Logger LOGGER = LoggerFactory.getLogger(StorageResourceController.class); + + private final StorageResourceRepository storageResourceRepository; + + public StorageResourceController(StorageResourceRepository storageResourceRepository) { + this.storageResourceRepository = storageResourceRepository; + } + + @Operation(summary = "Get all public storage resources with pagination") + @GetMapping("/public") + public ResponseEntity> getStorageResources( + @RequestParam(value = "pageNumber", defaultValue = "0") int pageNumber, + @RequestParam(value = "pageSize", defaultValue = "10") int pageSize, + @RequestParam(value = "nameSearch", required = false) String nameSearch, + @RequestParam(value = "tag", required = false) String[] tags) { + + LOGGER.info("Getting storage resources - page: {}, size: {}, search: {}", pageNumber, pageSize, nameSearch); + + Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending()); + Page resources; + + if (nameSearch != null && !nameSearch.trim().isEmpty()) { + resources = storageResourceRepository.findByNameSearchAndIsPublicTrueAndIsActiveTrue(nameSearch, pageable); + } else { + resources = storageResourceRepository.findByIsPublicTrueAndIsActiveTrue(pageable); + } + + LOGGER.info("Found {} storage resources", resources.getTotalElements()); + return ResponseEntity.ok(resources); + } + + @Operation(summary = "Get storage resource by ID") + @GetMapping("/public/{id}") + public ResponseEntity getStorageResourceById(@PathVariable("id") String id) { + LOGGER.info("Getting storage resource by ID: {}", id); + + Optional resource = storageResourceRepository.findById(id); + if (resource.isPresent()) { + return ResponseEntity.ok(resource.get()); + } else { + LOGGER.warn("Storage resource not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + } + + @Operation(summary = "Create new storage resource") + @PostMapping("/") + public ResponseEntity createStorageResource(@Valid @RequestBody StorageResource storageResource, BindingResult bindingResult) { + LOGGER.info("Creating new storage resource: {}", storageResource.getName()); + + // Validation error handling + if (bindingResult.hasErrors()) { + String errorMessage = bindingResult.getFieldErrors().stream() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .reduce((msg1, msg2) -> msg1 + ", " + msg2) + .orElse("Validation failed"); + LOGGER.error("Validation errors: {}", errorMessage); + return ResponseEntity.badRequest().body("Validation failed: " + errorMessage); + } + + try { + // Set default values for fields that might be null + if (storageResource.getCapacityTB() == null) { + storageResource.setCapacityTB(1L); // Default to 1 TB + } + if (storageResource.getSupportsEncryption() == null) { + storageResource.setSupportsEncryption(false); + } + if (storageResource.getSupportsVersioning() == null) { + storageResource.setSupportsVersioning(false); + } + if (storageResource.getIsPublic() == null) { + storageResource.setIsPublic(true); + } + if (storageResource.getIsActive() == null) { + storageResource.setIsActive(true); + } + + StorageResource savedResource = storageResourceRepository.save(storageResource); + LOGGER.info("Created storage resource with ID: {}", savedResource.getId()); + + return ResponseEntity.status(HttpStatus.CREATED).body(savedResource); + } catch (Exception e) { + LOGGER.error("Error creating storage resource: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error creating storage resource: " + e.getMessage()); + } + } + + @Operation(summary = "Update storage resource") + @PutMapping("/{id}") + public ResponseEntity updateStorageResource(@PathVariable("id") String id, @Valid @RequestBody StorageResource storageResource, BindingResult bindingResult) { + LOGGER.info("Updating storage resource with ID: {}", id); + + // Validation error handling + if (bindingResult.hasErrors()) { + String errorMessage = bindingResult.getFieldErrors().stream() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .reduce((msg1, msg2) -> msg1 + ", " + msg2) + .orElse("Validation failed"); + LOGGER.error("Validation errors: {}", errorMessage); + return ResponseEntity.badRequest().body("Validation failed: " + errorMessage); + } + + try { + Optional existingResource = storageResourceRepository.findById(id); + if (!existingResource.isPresent()) { + LOGGER.warn("Storage resource not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + + // Set the ID to ensure we update the correct resource + storageResource.setId(id); + + // Preserve creation timestamp + storageResource.setCreatedAt(existingResource.get().getCreatedAt()); + + StorageResource updatedResource = storageResourceRepository.save(storageResource); + LOGGER.info("Successfully updated storage resource with ID: {}", id); + + return ResponseEntity.ok(updatedResource); + } catch (Exception e) { + LOGGER.error("Error updating storage resource with ID: {}", id, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error updating storage resource: " + e.getMessage()); + } + } + + @Operation(summary = "Delete storage resource") + @DeleteMapping("/{id}") + public ResponseEntity deleteStorageResource(@PathVariable("id") String id) { + LOGGER.info("Deleting storage resource with ID: {}", id); + + try { + Optional existingResource = storageResourceRepository.findById(id); + if (!existingResource.isPresent()) { + LOGGER.warn("Storage resource not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + + storageResourceRepository.deleteById(id); + LOGGER.info("Successfully deleted storage resource with ID: {}", id); + return ResponseEntity.ok().body("Storage resource deleted successfully"); + } catch (Exception e) { + LOGGER.error("Error deleting storage resource with ID: {}", id, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error deleting storage resource: " + e.getMessage()); + } + } + + @Operation(summary = "Search storage resources by keyword") + @GetMapping("/search") + public ResponseEntity> searchStorageResources( + @RequestParam(value = "keyword") String keyword) { + + LOGGER.info("Searching storage resources with keyword: {}", keyword); + + List resources = storageResourceRepository + .findByNameContainingIgnoreCaseAndIsPublicTrue(keyword); + + LOGGER.info("Found {} storage resources matching keyword: {}", resources.size(), keyword); + return ResponseEntity.ok(resources); + } + + @Operation(summary = "Get storage resources by type") + @GetMapping("/type/{storageType}") + public ResponseEntity> getStorageResourcesByType( + @PathVariable("storageType") String storageType) { + + LOGGER.info("Getting storage resources by type: {}", storageType); + + List resources = storageResourceRepository + .findByStorageTypeAndIsPublicTrue(storageType); + + LOGGER.info("Found {} storage resources of type: {}", resources.size(), storageType); + return ResponseEntity.ok(resources); + } + + @Operation(summary = "Star/unstar a storage resource") + @PostMapping("/{id}/star") + public ResponseEntity starStorageResource(@PathVariable("id") String id) { + LOGGER.info("Starring storage resource with ID: {}", id); + // For now, just return true - starring functionality can be implemented later + return ResponseEntity.ok(true); + } + + @Operation(summary = "Check if user starred a storage resource") + @GetMapping("/{id}/star") + public ResponseEntity checkStorageResourceStarred(@PathVariable("id") String id) { + LOGGER.info("Checking if storage resource is starred: {}", id); + // For now, just return false - starring functionality can be implemented later + return ResponseEntity.ok(false); + } + + @Operation(summary = "Get storage resource star count") + @GetMapping("/{id}/stars/count") + public ResponseEntity getStorageResourceStarCount(@PathVariable("id") String id) { + LOGGER.info("Getting star count for storage resource: {}", id); + // For now, just return 0 - starring functionality can be implemented later + return ResponseEntity.ok(0L); + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java new file mode 100644 index 00000000000..0ffa38feae7 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java @@ -0,0 +1,253 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.Instant; +import java.util.List; +import org.hibernate.annotations.UuidGenerator; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Entity +@Table(name = "COMPUTE_RESOURCE_V2") +@EntityListeners(AuditingEntityListener.class) +public class ComputeResource { + + @Id + @GeneratedValue + @UuidGenerator + @Column(nullable = false, updatable = false, length = 48) + private String id; + + @Column(nullable = false) + @NotBlank(message = "Name is required") + @Size(max = 255, message = "Name must not exceed 255 characters") + private String name; + + @Column(nullable = false, columnDefinition = "TEXT") + @NotBlank(message = "Description is required") + @Size(max = 5000, message = "Description must not exceed 5000 characters") + private String description; + + @Column(nullable = false) + @NotBlank(message = "Hostname is required") + @Size(max = 255, message = "Hostname must not exceed 255 characters") + private String hostname; + + @Column(nullable = false) + @NotBlank(message = "Compute type is required") + @Size(max = 100, message = "Compute type must not exceed 100 characters") + private String computeType; // HPC, Cloud, Local, etc. + + @Column(nullable = false) + @NotNull(message = "CPU cores is required") + @Min(value = 1, message = "CPU cores must be at least 1") + private Integer cpuCores; + + @Column(nullable = false) + @NotNull(message = "Memory GB is required") + @Min(value = 1, message = "Memory GB must be at least 1") + private Integer memoryGB; + + @Column(nullable = false) + @NotBlank(message = "Operating system is required") + @Size(max = 100, message = "Operating system must not exceed 100 characters") + private String operatingSystem; + + @Column(nullable = false) + @NotBlank(message = "Queue system is required") + @Size(max = 100, message = "Queue system must not exceed 100 characters") + private String queueSystem; // SLURM, PBS, SGE, etc. + + @Column(columnDefinition = "TEXT") + private String additionalInfo; + + @Column(nullable = false) + private Boolean isPublic = true; + + @Column(nullable = false) + private Boolean isActive = true; + + @Column(nullable = false) + @NotBlank(message = "Resource manager is required") + @Size(max = 255, message = "Resource manager must not exceed 255 characters") + private String resourceManager; // Gateway name or organization + + @Column(nullable = false, updatable = false) + @CreatedDate + private Instant createdAt; + + @Column(nullable = false) + @LastModifiedDate + private Instant updatedAt; + + // Default constructor + public ComputeResource() {} + + // Constructor for mock data creation + public ComputeResource(String name, String description, String hostname, String computeType, + Integer cpuCores, Integer memoryGB, String operatingSystem, + String queueSystem, String additionalInfo, String resourceManager) { + this.name = name; + this.description = description; + this.hostname = hostname; + this.computeType = computeType; + this.cpuCores = cpuCores; + this.memoryGB = memoryGB; + this.operatingSystem = operatingSystem; + this.queueSystem = queueSystem; + this.additionalInfo = additionalInfo; + this.resourceManager = resourceManager; + this.isPublic = true; + this.isActive = true; + } + + // Getters and Setters + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getHostname() { + return hostname; + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + public String getComputeType() { + return computeType; + } + + public void setComputeType(String computeType) { + this.computeType = computeType; + } + + public Integer getCpuCores() { + return cpuCores; + } + + public void setCpuCores(Integer cpuCores) { + this.cpuCores = cpuCores; + } + + public Integer getMemoryGB() { + return memoryGB; + } + + public void setMemoryGB(Integer memoryGB) { + this.memoryGB = memoryGB; + } + + public String getOperatingSystem() { + return operatingSystem; + } + + public void setOperatingSystem(String operatingSystem) { + this.operatingSystem = operatingSystem; + } + + public String getQueueSystem() { + return queueSystem; + } + + public void setQueueSystem(String queueSystem) { + this.queueSystem = queueSystem; + } + + public String getAdditionalInfo() { + return additionalInfo; + } + + public void setAdditionalInfo(String additionalInfo) { + this.additionalInfo = additionalInfo; + } + + public Boolean getIsPublic() { + return isPublic; + } + + public void setIsPublic(Boolean isPublic) { + this.isPublic = isPublic; + } + + public Boolean getIsActive() { + return isActive; + } + + public void setIsActive(Boolean isActive) { + this.isActive = isActive; + } + + public String getResourceManager() { + return resourceManager; + } + + public void setResourceManager(String resourceManager) { + this.resourceManager = resourceManager; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java new file mode 100644 index 00000000000..b9e05c2bb6f --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java @@ -0,0 +1,263 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.Instant; +import org.hibernate.annotations.UuidGenerator; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Entity +@Table(name = "STORAGE_RESOURCE_V2") +@EntityListeners(AuditingEntityListener.class) +public class StorageResource { + + @Id + @GeneratedValue + @UuidGenerator + @Column(nullable = false, updatable = false, length = 48) + private String id; + + @Column(nullable = false) + @NotBlank(message = "Name is required") + @Size(max = 255, message = "Name must not exceed 255 characters") + private String name; + + @Column(nullable = false, columnDefinition = "TEXT") + @NotBlank(message = "Description is required") + @Size(max = 5000, message = "Description must not exceed 5000 characters") + private String description; + + @Column(nullable = false) + @NotBlank(message = "Hostname is required") + @Size(max = 255, message = "Hostname must not exceed 255 characters") + private String hostname; + + @Column(nullable = false) + @NotBlank(message = "Storage type is required") + @Size(max = 100, message = "Storage type must not exceed 100 characters") + private String storageType; // Object Storage, File System, Database, etc. + + @Column(nullable = false) + @NotNull(message = "Capacity TB is required") + @Min(value = 1, message = "Capacity TB must be at least 1") + private Long capacityTB; + + @Column(nullable = false) + @NotBlank(message = "Access protocol is required") + @Size(max = 100, message = "Access protocol must not exceed 100 characters") + private String accessProtocol; // S3, SFTP, NFS, HTTP, etc. + + @Column(nullable = false) + @NotBlank(message = "Endpoint is required") + @Size(max = 500, message = "Endpoint must not exceed 500 characters") + private String endpoint; // API endpoint or mount point + + @Column(nullable = false) + private Boolean supportsEncryption = false; + + @Column(nullable = false) + private Boolean supportsVersioning = false; + + @Column(columnDefinition = "TEXT") + private String additionalInfo; + + @Column(nullable = false) + private Boolean isPublic = true; + + @Column(nullable = false) + private Boolean isActive = true; + + @Column(nullable = false) + @NotBlank(message = "Resource manager is required") + @Size(max = 255, message = "Resource manager must not exceed 255 characters") + private String resourceManager; // Gateway name or organization + + @Column(nullable = false, updatable = false) + @CreatedDate + private Instant createdAt; + + @Column(nullable = false) + @LastModifiedDate + private Instant updatedAt; + + // Default constructor + public StorageResource() {} + + // Constructor for mock data creation + public StorageResource(String name, String description, String hostname, String storageType, + Long capacityTB, String accessProtocol, String endpoint, + Boolean supportsEncryption, Boolean supportsVersioning, + String additionalInfo, String resourceManager) { + this.name = name; + this.description = description; + this.hostname = hostname; + this.storageType = storageType; + this.capacityTB = capacityTB; + this.accessProtocol = accessProtocol; + this.endpoint = endpoint; + this.supportsEncryption = supportsEncryption; + this.supportsVersioning = supportsVersioning; + this.additionalInfo = additionalInfo; + this.resourceManager = resourceManager; + this.isPublic = true; + this.isActive = true; + } + + // Getters and Setters + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getHostname() { + return hostname; + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + public String getStorageType() { + return storageType; + } + + public void setStorageType(String storageType) { + this.storageType = storageType; + } + + public Long getCapacityTB() { + return capacityTB; + } + + public void setCapacityTB(Long capacityTB) { + this.capacityTB = capacityTB; + } + + public String getAccessProtocol() { + return accessProtocol; + } + + public void setAccessProtocol(String accessProtocol) { + this.accessProtocol = accessProtocol; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public Boolean getSupportsEncryption() { + return supportsEncryption; + } + + public void setSupportsEncryption(Boolean supportsEncryption) { + this.supportsEncryption = supportsEncryption; + } + + public Boolean getSupportsVersioning() { + return supportsVersioning; + } + + public void setSupportsVersioning(Boolean supportsVersioning) { + this.supportsVersioning = supportsVersioning; + } + + public String getAdditionalInfo() { + return additionalInfo; + } + + public void setAdditionalInfo(String additionalInfo) { + this.additionalInfo = additionalInfo; + } + + public Boolean getIsPublic() { + return isPublic; + } + + public void setIsPublic(Boolean isPublic) { + this.isPublic = isPublic; + } + + public Boolean getIsActive() { + return isActive; + } + + public void setIsActive(Boolean isActive) { + this.isActive = isActive; + } + + public String getResourceManager() { + return resourceManager; + } + + public void setResourceManager(String resourceManager) { + this.resourceManager = resourceManager; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java new file mode 100644 index 00000000000..8cea8069de9 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java @@ -0,0 +1,52 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.repository; + +import java.util.List; +import org.apache.airavata.research.service.v2.entity.ComputeResource; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface ComputeResourceRepository extends JpaRepository { + + // Find by name containing (case insensitive) + List findByNameContainingIgnoreCaseAndIsPublicTrue(String name); + + // Find by compute type + List findByComputeTypeAndIsPublicTrue(String computeType); + + // Find all public and active resources with pagination + Page findByIsPublicTrueAndIsActiveTrue(Pageable pageable); + + // Search by name with pagination + @Query("SELECT c FROM ComputeResource c WHERE c.isPublic = true AND c.isActive = true AND " + + "(LOWER(c.name) LIKE LOWER(CONCAT('%', :nameSearch, '%')) OR " + + "LOWER(c.description) LIKE LOWER(CONCAT('%', :nameSearch, '%')) OR " + + "LOWER(c.computeType) LIKE LOWER(CONCAT('%', :nameSearch, '%')))") + Page findByNameSearchAndIsPublicTrueAndIsActiveTrue(@Param("nameSearch") String nameSearch, Pageable pageable); + + // Find all public and active resources + List findAllByIsPublicTrueAndIsActiveTrue(); +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/StorageResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/StorageResourceRepository.java new file mode 100644 index 00000000000..cf2fd2810b5 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/StorageResourceRepository.java @@ -0,0 +1,52 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.repository; + +import java.util.List; +import org.apache.airavata.research.service.v2.entity.StorageResource; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface StorageResourceRepository extends JpaRepository { + + // Find by name containing (case insensitive) + List findByNameContainingIgnoreCaseAndIsPublicTrue(String name); + + // Find by storage type + List findByStorageTypeAndIsPublicTrue(String storageType); + + // Find all public and active resources with pagination + Page findByIsPublicTrueAndIsActiveTrue(Pageable pageable); + + // Search by name with pagination + @Query("SELECT s FROM StorageResource s WHERE s.isPublic = true AND s.isActive = true AND " + + "(LOWER(s.name) LIKE LOWER(CONCAT('%', :nameSearch, '%')) OR " + + "LOWER(s.description) LIKE LOWER(CONCAT('%', :nameSearch, '%')) OR " + + "LOWER(s.storageType) LIKE LOWER(CONCAT('%', :nameSearch, '%')))") + Page findByNameSearchAndIsPublicTrueAndIsActiveTrue(@Param("nameSearch") String nameSearch, Pageable pageable); + + // Find all public and active resources + List findAllByIsPublicTrueAndIsActiveTrue(); +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/resources/application.yml b/modules/research-framework/research-service/src/main/resources/application.yml index 0652644a64e..cb803755096 100644 --- a/modules/research-framework/research-service/src/main/resources/application.yml +++ b/modules/research-framework/research-service/src/main/resources/application.yml @@ -19,7 +19,7 @@ grpc: port: 19908 server: - port: 18889 + port: 8080 address: 0.0.0.0 airavata: @@ -44,17 +44,22 @@ spring: max-file-size: 200MB max-request-size: 200MB datasource: - url: "jdbc:mariadb://airavata.host:13306/research_catalog" - username: "airavata" - password: "123456" - driver-class-name: org.mariadb.jdbc.Driver + url: "jdbc:h2:mem:testdb" + username: "sa" + password: "" + driver-class-name: org.h2.Driver hikari: pool-name: ResearchCatalogPool leak-detection-threshold: 20000 jpa: hibernate: - ddl-auto: update + ddl-auto: create-drop open-in-view: false + show-sql: true + h2: + console: + enabled: true + path: /h2-console springdoc: api-docs: From 261e67539391ce1cbeded9f5b151bd2f29dff95f Mon Sep 17 00:00:00 2001 From: Krish Katariya Date: Sun, 27 Jul 2025 17:06:52 -0700 Subject: [PATCH 02/17] Removed old admin api work --- .../.gradle/8.14.3/checksums/checksums.lock | Bin 17 -> 0 bytes .../8.14.3/checksums/md5-checksums.bin | Bin 33747 -> 0 bytes .../8.14.3/checksums/sha1-checksums.bin | Bin 83831 -> 0 bytes .../executionHistory/executionHistory.bin | Bin 256724 -> 0 bytes .../executionHistory/executionHistory.lock | Bin 17 -> 0 bytes .../.gradle/8.14.3/fileChanges/last-build.bin | Bin 1 -> 0 bytes .../.gradle/8.14.3/fileHashes/fileHashes.bin | Bin 22397 -> 0 bytes .../.gradle/8.14.3/fileHashes/fileHashes.lock | Bin 17 -> 0 bytes .../8.14.3/fileHashes/resourceHashesCache.bin | Bin 20979 -> 0 bytes .../.gradle/8.14.3/gc.properties | 0 .../buildOutputCleanup.lock | Bin 17 -> 0 bytes .../buildOutputCleanup/cache.properties | 2 -- .../buildOutputCleanup/outputFiles.bin | Bin 18947 -> 0 bytes .../.gradle/file-system.probe | Bin 8 -> 0 bytes .../.gradle/vcs-1/gc.properties | 0 modules/admin-api-server/HELP.md | 31 ------------------ 16 files changed, 33 deletions(-) delete mode 100644 modules/admin-api-server/.gradle/8.14.3/checksums/checksums.lock delete mode 100644 modules/admin-api-server/.gradle/8.14.3/checksums/md5-checksums.bin delete mode 100644 modules/admin-api-server/.gradle/8.14.3/checksums/sha1-checksums.bin delete mode 100644 modules/admin-api-server/.gradle/8.14.3/executionHistory/executionHistory.bin delete mode 100644 modules/admin-api-server/.gradle/8.14.3/executionHistory/executionHistory.lock delete mode 100644 modules/admin-api-server/.gradle/8.14.3/fileChanges/last-build.bin delete mode 100644 modules/admin-api-server/.gradle/8.14.3/fileHashes/fileHashes.bin delete mode 100644 modules/admin-api-server/.gradle/8.14.3/fileHashes/fileHashes.lock delete mode 100644 modules/admin-api-server/.gradle/8.14.3/fileHashes/resourceHashesCache.bin delete mode 100644 modules/admin-api-server/.gradle/8.14.3/gc.properties delete mode 100644 modules/admin-api-server/.gradle/buildOutputCleanup/buildOutputCleanup.lock delete mode 100644 modules/admin-api-server/.gradle/buildOutputCleanup/cache.properties delete mode 100644 modules/admin-api-server/.gradle/buildOutputCleanup/outputFiles.bin delete mode 100644 modules/admin-api-server/.gradle/file-system.probe delete mode 100644 modules/admin-api-server/.gradle/vcs-1/gc.properties delete mode 100644 modules/admin-api-server/HELP.md diff --git a/modules/admin-api-server/.gradle/8.14.3/checksums/checksums.lock b/modules/admin-api-server/.gradle/8.14.3/checksums/checksums.lock deleted file mode 100644 index 91b779c3a9fb78cabe5b374c64cd88f7a169d91a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17 VcmZQpa{N1q_hmsB0~oNF0st?G1RnqZ diff --git a/modules/admin-api-server/.gradle/8.14.3/checksums/md5-checksums.bin b/modules/admin-api-server/.gradle/8.14.3/checksums/md5-checksums.bin deleted file mode 100644 index 660ea1094523704f8855c53df58e01e033100820..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33747 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 diff --git a/modules/admin-api-server/.gradle/8.14.3/executionHistory/executionHistory.lock b/modules/admin-api-server/.gradle/8.14.3/executionHistory/executionHistory.lock deleted file mode 100644 index eecb9426f9f4b6f258972e09d3d7229f84840af9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17 UcmZQ}zyHu^|Ex{i3=m)r05`n^iU0rr diff --git a/modules/admin-api-server/.gradle/8.14.3/fileChanges/last-build.bin b/modules/admin-api-server/.gradle/8.14.3/fileChanges/last-build.bin deleted file mode 100644 index f76dd238ade08917e6712764a16a22005a50573d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1 IcmZPo000310RR91 diff --git a/modules/admin-api-server/.gradle/8.14.3/fileHashes/fileHashes.bin b/modules/admin-api-server/.gradle/8.14.3/fileHashes/fileHashes.bin deleted file mode 100644 index 2164ecb07c1fb6e4a6cf6aebd94778f8e0b0457c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22397 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 diff --git a/modules/admin-api-server/.gradle/8.14.3/fileHashes/fileHashes.lock b/modules/admin-api-server/.gradle/8.14.3/fileHashes/fileHashes.lock deleted file mode 100644 index c705cfc2b9028bf7c288383ea7f9254af896e7fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17 VcmZQxc7yf3-CDmA1~6bW0RS~(1Xut7 diff --git a/modules/admin-api-server/.gradle/8.14.3/fileHashes/resourceHashesCache.bin b/modules/admin-api-server/.gradle/8.14.3/fileHashes/resourceHashesCache.bin deleted file mode 100644 index f7c24a8b7dca851949b3a5290453bf9ef06ca09b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20979 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 l5STB4&CW)7&YdN Date: Mon, 28 Jul 2025 22:18:21 -0700 Subject: [PATCH 03/17] Adding 'code' section to portal --- .../service/v2/config/V2DataInitializer.java | 170 +++++++- .../service/v2/controller/CodeController.java | 405 ++++++++++++++++++ .../research/service/v2/entity/Code.java | 298 +++++++++++++ .../service/v2/repository/CodeRepository.java | 92 ++++ 4 files changed, 963 insertions(+), 2 deletions(-) create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/CodeRepository.java diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java index d61044f28c8..2e84a66a9c9 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java @@ -21,8 +21,11 @@ import jakarta.annotation.PostConstruct; import java.util.List; +import java.util.Set; +import org.apache.airavata.research.service.v2.entity.Code; import org.apache.airavata.research.service.v2.entity.ComputeResource; import org.apache.airavata.research.service.v2.entity.StorageResource; +import org.apache.airavata.research.service.v2.repository.CodeRepository; import org.apache.airavata.research.service.v2.repository.ComputeResourceRepository; import org.apache.airavata.research.service.v2.repository.StorageResourceRepository; import org.slf4j.Logger; @@ -36,19 +39,23 @@ public class V2DataInitializer { private final ComputeResourceRepository computeResourceRepository; private final StorageResourceRepository storageResourceRepository; + private final CodeRepository codeRepository; public V2DataInitializer(ComputeResourceRepository computeResourceRepository, - StorageResourceRepository storageResourceRepository) { + StorageResourceRepository storageResourceRepository, + CodeRepository codeRepository) { this.computeResourceRepository = computeResourceRepository; this.storageResourceRepository = storageResourceRepository; + this.codeRepository = codeRepository; } @PostConstruct public void initializeData() { - LOGGER.info("Initializing V2 mock data for compute and storage resources..."); + LOGGER.info("Initializing V2 mock data for compute, storage, and code resources..."); initializeComputeResources(); initializeStorageResources(); + initializeCodes(); LOGGER.info("V2 mock data initialization completed."); } @@ -317,4 +324,163 @@ private void initializeStorageResources() { LOGGER.info("Created {} storage resources", storageResources.size()); } } + + private void initializeCodes() { + if (codeRepository.count() == 0) { + LOGGER.info("Creating mock code resources..."); + + List codes = List.of( + // Model-type codes + new Code( + "COVID-19 Chest X-ray Classification Model", + "Deep learning model for automatic detection of COVID-19 pneumonia from chest X-ray images using ResNet-50 architecture with transfer learning.", + "MODEL", + "Python", + "TensorFlow", + Set.of("dr.sarah.medical@stanford.edu", "alex.vision@mit.edu"), + Set.of("medical", "computer-vision", "covid-19", "deep-learning", "classification") + ), + + new Code( + "Financial Fraud Detection Model", + "Machine learning ensemble model combining XGBoost and Random Forest for real-time credit card fraud detection with 99.2% accuracy.", + "MODEL", + "Python", + "Scikit-learn", + Set.of("mike.finance@jpmorgan.com", "lisa.ml@visa.com"), + Set.of("finance", "fraud-detection", "machine-learning", "ensemble", "xgboost") + ), + + new Code( + "Protein Folding Prediction Model", + "AlphaFold2-inspired neural network for predicting 3D protein structures from amino acid sequences using attention mechanisms.", + "MODEL", + "Python", + "PyTorch", + Set.of("prof.chen@deepmind.com", "bio.researcher@harvard.edu"), + Set.of("bioinformatics", "protein-folding", "deep-learning", "attention", "alphafold") + ), + + // Notebook-type codes + new Code( + "Cybersecurity Threat Analysis Notebook", + "Comprehensive Jupyter notebook for analyzing network traffic patterns and identifying potential cybersecurity threats using statistical analysis.", + "NOTEBOOK", + "Python", + null, + Set.of("security.analyst@cisco.com", "threat.hunter@crowdstrike.com"), + Set.of("cybersecurity", "threat-analysis", "network-security", "statistical-analysis", "jupyter") + ), + + new Code( + "Climate Data Visualization Notebook", + "Interactive data visualization notebook for climate change analysis using NOAA datasets with advanced plotting and statistical modeling.", + "NOTEBOOK", + "Python", + null, + Set.of("climate.scientist@noaa.gov", "data.viz@nasa.gov"), + Set.of("climate-science", "data-visualization", "environmental", "statistical-modeling", "matplotlib") + ), + + new Code( + "NLP Sentiment Analysis Notebook", + "End-to-end natural language processing pipeline for sentiment analysis of social media data using transformer models and BERT.", + "NOTEBOOK", + "Python", + null, + Set.of("nlp.researcher@google.com", "sentiment.expert@twitter.com"), + Set.of("nlp", "sentiment-analysis", "transformers", "bert", "social-media") + ), + + // Repository-type codes + new Code( + "Distributed Machine Learning Framework", + "Open-source framework for distributed machine learning across multiple compute nodes with fault tolerance and auto-scaling capabilities.", + "REPOSITORY", + "Python", + "PyTorch", + Set.of("distributed.ml@uber.com", "framework.dev@netflix.com"), + Set.of("distributed-computing", "machine-learning", "framework", "pytorch", "scalability") + ), + + new Code( + "Quantum Computing Algorithms Library", + "Comprehensive library of quantum computing algorithms implemented in Qiskit with educational examples and benchmarking tools.", + "REPOSITORY", + "Python", + "Qiskit", + Set.of("quantum.researcher@ibm.com", "algorithms.expert@google.com"), + Set.of("quantum-computing", "algorithms", "qiskit", "benchmarking", "education") + ), + + new Code( + "Time Series Forecasting Toolkit", + "Advanced time series analysis and forecasting toolkit with support for ARIMA, LSTM, and Prophet models for financial and IoT data.", + "REPOSITORY", + "Python", + "TensorFlow", + Set.of("time.series@bloomberg.com", "forecasting.expert@amazon.com"), + Set.of("time-series", "forecasting", "arima", "lstm", "prophet", "financial") + ) + ); + + // Set additional properties for codes + for (int i = 0; i < codes.size(); i++) { + Code code = codes.get(i); + + // Add some random star counts for demonstration + int starCount = (int) (Math.random() * 1000) + 10; + code.setStarCount(starCount); + + // Set type-specific fields + if ("MODEL".equals(code.getCodeType())) { + if (i == 0) { // COVID model + code.setApplicationInterfaceId("covid_xray_classifier_v2.1"); + code.setVersion("2.1"); + } else if (i == 1) { // Fraud model + code.setApplicationInterfaceId("fraud_detector_ensemble_v1.3"); + code.setVersion("1.3"); + } else if (i == 2) { // Protein model + code.setApplicationInterfaceId("protein_fold_predictor_v3.0"); + code.setVersion("3.0"); + } + code.setAdditionalInfo("Pre-trained model weights available. Compatible with standard ML pipelines."); + } else if ("NOTEBOOK".equals(code.getCodeType())) { + if (i == 3) { // Cybersecurity notebook + code.setNotebookPath("/notebooks/cybersecurity/threat_analysis.ipynb"); + } else if (i == 4) { // Climate notebook + code.setNotebookPath("/notebooks/climate/climate_visualization.ipynb"); + } else if (i == 5) { // NLP notebook + code.setNotebookPath("/notebooks/nlp/sentiment_analysis.ipynb"); + } + code.setAdditionalInfo("Interactive Jupyter notebook with step-by-step analysis and visualizations."); + } else if ("REPOSITORY".equals(code.getCodeType())) { + if (i == 6) { // ML Framework + code.setRepositoryUrl("https://github.com/ml-distributed/framework"); + } else if (i == 7) { // Quantum library + code.setRepositoryUrl("https://github.com/quantum-algorithms/qiskit-library"); + } else if (i == 8) { // Time series toolkit + code.setRepositoryUrl("https://github.com/timeseries-toolkit/forecasting"); + } + code.setAdditionalInfo("Full source code repository with documentation, tests, and CI/CD pipeline."); + } + + // Add some dependencies based on programming language and framework + if ("Python".equals(code.getProgrammingLanguage())) { + code.getDependencies().addAll(Set.of("numpy", "pandas", "matplotlib")); + + if ("TensorFlow".equals(code.getFramework())) { + code.getDependencies().addAll(Set.of("tensorflow>=2.8.0", "keras")); + } else if ("PyTorch".equals(code.getFramework())) { + code.getDependencies().addAll(Set.of("torch>=1.12.0", "torchvision")); + } else if ("Scikit-learn".equals(code.getFramework())) { + code.getDependencies().addAll(Set.of("scikit-learn>=1.1.0", "joblib")); + } + } + } + + codeRepository.saveAll(codes); + LOGGER.info("Created {} code resources", codes.size()); + } + } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java new file mode 100644 index 00000000000..047d731d652 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java @@ -0,0 +1,405 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import java.util.Optional; +import jakarta.validation.Valid; +import org.apache.airavata.research.service.v2.entity.Code; +import org.apache.airavata.research.service.v2.repository.CodeRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v2/rf/codes") +@Tag(name = "Code Resources V2", description = "V2 API for managing code resources (models, notebooks, repositories)") +public class CodeController { + + private static final Logger LOGGER = LoggerFactory.getLogger(CodeController.class); + + private final CodeRepository codeRepository; + + public CodeController(CodeRepository codeRepository) { + this.codeRepository = codeRepository; + } + + @Operation(summary = "Get all public codes with pagination") + @GetMapping("/public") + public ResponseEntity> getCodes( + @RequestParam(value = "pageNumber", defaultValue = "0") int pageNumber, + @RequestParam(value = "pageSize", defaultValue = "10") int pageSize, + @RequestParam(value = "keyword", required = false) String keyword, + @RequestParam(value = "codeType", required = false) String codeType, + @RequestParam(value = "programmingLanguage", required = false) String programmingLanguage) { + + LOGGER.info("Getting codes - page: {}, size: {}, keyword: {}, type: {}, language: {}", + pageNumber, pageSize, keyword, codeType, programmingLanguage); + + Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending()); + Page codes; + + if (keyword != null && !keyword.trim().isEmpty()) { + codes = codeRepository.findByKeywordSearchAndIsPublicTrueAndIsActiveTrue(keyword, pageable); + } else { + codes = codeRepository.findByIsPublicTrueAndIsActiveTrue(pageable); + } + + LOGGER.info("Found {} codes", codes.getTotalElements()); + return ResponseEntity.ok(codes); + } + + @Operation(summary = "Get code by ID") + @GetMapping("/public/{id}") + public ResponseEntity getCodeById(@PathVariable("id") String id) { + LOGGER.info("Getting code by ID: {}", id); + + Optional code = codeRepository.findById(id); + if (code.isPresent()) { + return ResponseEntity.ok(code.get()); + } else { + LOGGER.warn("Code not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + } + + @Operation(summary = "Create new code") + @PostMapping("/") + public ResponseEntity createCode(@Valid @RequestBody Code code, BindingResult bindingResult) { + LOGGER.info("Creating new code: {}", code.getName()); + + // Validation error handling + if (bindingResult.hasErrors()) { + String errorMessage = bindingResult.getFieldErrors().stream() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .reduce((msg1, msg2) -> msg1 + ", " + msg2) + .orElse("Validation failed"); + LOGGER.error("Validation errors: {}", errorMessage); + return ResponseEntity.badRequest().body("Validation failed: " + errorMessage); + } + + try { + // Set default values for fields that might be null + if (code.getIsPublic() == null) { + code.setIsPublic(true); + } + if (code.getIsActive() == null) { + code.setIsActive(true); + } + if (code.getStarCount() == null) { + code.setStarCount(0); + } + + Code savedCode = codeRepository.save(code); + LOGGER.info("Created code with ID: {}", savedCode.getId()); + + return ResponseEntity.status(HttpStatus.CREATED).body(savedCode); + } catch (Exception e) { + LOGGER.error("Error creating code: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error creating code: " + e.getMessage()); + } + } + + @Operation(summary = "Update code") + @PutMapping("/{id}") + public ResponseEntity updateCode(@PathVariable("id") String id, @Valid @RequestBody Code code, BindingResult bindingResult) { + LOGGER.info("Updating code with ID: {}", id); + + // Validation error handling + if (bindingResult.hasErrors()) { + String errorMessage = bindingResult.getFieldErrors().stream() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .reduce((msg1, msg2) -> msg1 + ", " + msg2) + .orElse("Validation failed"); + LOGGER.error("Validation errors: {}", errorMessage); + return ResponseEntity.badRequest().body("Validation failed: " + errorMessage); + } + + try { + Optional existingCode = codeRepository.findById(id); + if (!existingCode.isPresent()) { + LOGGER.warn("Code not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + + // Set the ID to ensure we update the correct code + code.setId(id); + + // Preserve creation timestamp and star count + code.setCreatedAt(existingCode.get().getCreatedAt()); + if (code.getStarCount() == null) { + code.setStarCount(existingCode.get().getStarCount()); + } + + Code updatedCode = codeRepository.save(code); + LOGGER.info("Successfully updated code with ID: {}", id); + + return ResponseEntity.ok(updatedCode); + } catch (Exception e) { + LOGGER.error("Error updating code with ID: {}", id, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error updating code: " + e.getMessage()); + } + } + + @Operation(summary = "Delete code") + @DeleteMapping("/{id}") + public ResponseEntity deleteCode(@PathVariable("id") String id) { + LOGGER.info("Deleting code with ID: {}", id); + + try { + Optional existingCode = codeRepository.findById(id); + if (!existingCode.isPresent()) { + LOGGER.warn("Code not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + + codeRepository.deleteById(id); + LOGGER.info("Successfully deleted code with ID: {}", id); + return ResponseEntity.ok().body("Code deleted successfully"); + } catch (Exception e) { + LOGGER.error("Error deleting code with ID: {}", id, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error deleting code: " + e.getMessage()); + } + } + + @Operation(summary = "Search codes by keyword") + @GetMapping("/search") + public ResponseEntity> searchCodes( + @RequestParam(value = "keyword") String keyword) { + + LOGGER.info("Searching codes with keyword: {}", keyword); + + List codes = codeRepository.findByKeywordSearchAndIsPublicTrueAndIsActiveTrue(keyword); + + LOGGER.info("Found {} codes matching keyword: {}", codes.size(), keyword); + return ResponseEntity.ok(codes); + } + + @Operation(summary = "Get codes by type") + @GetMapping("/type/{codeType}") + public ResponseEntity> getCodesByType( + @PathVariable("codeType") String codeType) { + + LOGGER.info("Getting codes by type: {}", codeType); + + List codes = codeRepository.findByCodeTypeAndIsPublicTrue(codeType); + + LOGGER.info("Found {} codes of type: {}", codes.size(), codeType); + return ResponseEntity.ok(codes); + } + + @Operation(summary = "Get codes by programming language") + @GetMapping("/language/{programmingLanguage}") + public ResponseEntity> getCodesByLanguage( + @PathVariable("programmingLanguage") String programmingLanguage) { + + LOGGER.info("Getting codes by programming language: {}", programmingLanguage); + + List codes = codeRepository.findByProgrammingLanguageAndIsPublicTrue(programmingLanguage); + + LOGGER.info("Found {} codes for language: {}", codes.size(), programmingLanguage); + return ResponseEntity.ok(codes); + } + + @Operation(summary = "Get codes by framework") + @GetMapping("/framework/{framework}") + public ResponseEntity> getCodesByFramework( + @PathVariable("framework") String framework) { + + LOGGER.info("Getting codes by framework: {}", framework); + + List codes = codeRepository.findByFrameworkAndIsPublicTrue(framework); + + LOGGER.info("Found {} codes for framework: {}", codes.size(), framework); + return ResponseEntity.ok(codes); + } + + @Operation(summary = "Get codes by tag") + @GetMapping("/tag/{tag}") + public ResponseEntity> getCodesByTag( + @PathVariable("tag") String tag) { + + LOGGER.info("Getting codes by tag: {}", tag); + + List codes = codeRepository.findByTagAndIsPublicTrue(tag); + + LOGGER.info("Found {} codes with tag: {}", codes.size(), tag); + return ResponseEntity.ok(codes); + } + + @Operation(summary = "Get codes by author") + @GetMapping("/author/{author}") + public ResponseEntity> getCodesByAuthor( + @PathVariable("author") String author) { + + LOGGER.info("Getting codes by author: {}", author); + + List codes = codeRepository.findByAuthorAndIsPublicTrue(author); + + LOGGER.info("Found {} codes by author: {}", codes.size(), author); + return ResponseEntity.ok(codes); + } + + @Operation(summary = "Get top starred codes") + @GetMapping("/top-starred") + public ResponseEntity> getTopStarredCodes( + @RequestParam(value = "limit", defaultValue = "10") int limit) { + + LOGGER.info("Getting top {} starred codes", limit); + + Pageable pageable = PageRequest.of(0, limit); + List codes = codeRepository.findTopStarredCodes(pageable); + + LOGGER.info("Found {} top starred codes", codes.size()); + return ResponseEntity.ok(codes); + } + + @Operation(summary = "Get recent codes") + @GetMapping("/recent") + public ResponseEntity> getRecentCodes( + @RequestParam(value = "limit", defaultValue = "10") int limit) { + + LOGGER.info("Getting {} recent codes", limit); + + Pageable pageable = PageRequest.of(0, limit); + List codes = codeRepository.findRecentCodes(pageable); + + LOGGER.info("Found {} recent codes", codes.size()); + return ResponseEntity.ok(codes); + } + + @Operation(summary = "Star/unstar a code") + @PostMapping("/{id}/star") + public ResponseEntity starCode(@PathVariable("id") String id) { + LOGGER.info("Toggling star for code with ID: {}", id); + + try { + Optional codeOpt = codeRepository.findById(id); + if (codeOpt.isPresent()) { + Code code = codeOpt.get(); + + // Simple toggle mechanism - if already starred (starCount > 0), unstar it + // For simplicity, we use starCount as a toggle indicator + boolean isCurrentlyStarred = code.getStarCount() > 0; + + if (isCurrentlyStarred) { + // Unstar: set count to 0 + code.setStarCount(0); + codeRepository.save(code); + LOGGER.info("Code unstarred: {}", id); + return ResponseEntity.ok(false); + } else { + // Star: set count to 1 + code.setStarCount(1); + codeRepository.save(code); + LOGGER.info("Code starred: {}", id); + return ResponseEntity.ok(true); + } + } else { + LOGGER.warn("Code not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + } catch (Exception e) { + LOGGER.error("Error toggling code star: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + @Operation(summary = "Check if user starred a code") + @GetMapping("/{id}/star") + public ResponseEntity checkCodeStarred(@PathVariable("id") String id) { + LOGGER.info("Checking if code is starred: {}", id); + + try { + Optional codeOpt = codeRepository.findById(id); + if (codeOpt.isPresent()) { + Code code = codeOpt.get(); + // Code is starred if starCount > 0 + boolean isStarred = code.getStarCount() > 0; + LOGGER.info("Code {} starred status: {}", id, isStarred); + return ResponseEntity.ok(isStarred); + } else { + LOGGER.warn("Code not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + } catch (Exception e) { + LOGGER.error("Error checking code star status: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + @Operation(summary = "Get code star count") + @GetMapping("/{id}/stars/count") + public ResponseEntity getCodeStarCount(@PathVariable("id") String id) { + LOGGER.info("Getting star count for code: {}", id); + + try { + Optional codeOpt = codeRepository.findById(id); + if (codeOpt.isPresent()) { + return ResponseEntity.ok(codeOpt.get().getStarCount()); + } else { + LOGGER.warn("Code not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + } catch (Exception e) { + LOGGER.error("Error getting star count: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + @Operation(summary = "Get all starred codes") + @GetMapping("/starred") + public ResponseEntity> getStarredCodes( + @RequestParam(value = "page", defaultValue = "0") int page, + @RequestParam(value = "size", defaultValue = "50") int size) { + LOGGER.info("Fetching starred codes - page: {}, size: {}", page, size); + + try { + Pageable pageable = PageRequest.of(page, size); + // Get codes where starCount > 0 (i.e., starred codes) + Page starredCodes = codeRepository.findByStarCountGreaterThanAndIsPublicTrueAndIsActiveTrue(0, pageable); + LOGGER.info("Found {} starred codes", starredCodes.getTotalElements()); + return ResponseEntity.ok(starredCodes); + } catch (Exception e) { + LOGGER.error("Error fetching starred codes: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java new file mode 100644 index 00000000000..fd52263336d --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java @@ -0,0 +1,298 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.entity; + +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import java.time.Instant; +import java.util.HashSet; +import java.util.Set; +import org.hibernate.annotations.UuidGenerator; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Entity +@Table(name = "CODE_V2") +@EntityListeners(AuditingEntityListener.class) +public class Code { + + @Id + @GeneratedValue + @UuidGenerator + @Column(nullable = false, updatable = false, length = 48) + private String id; + + @Column(nullable = false) + @NotBlank(message = "Name is required") + @Size(max = 255, message = "Name must not exceed 255 characters") + private String name; + + @Column(nullable = false, columnDefinition = "TEXT") + @NotBlank(message = "Description is required") + @Size(max = 5000, message = "Description must not exceed 5000 characters") + private String description; + + @Column(nullable = false) + @NotBlank(message = "Code type is required") + @Size(max = 100, message = "Code type must not exceed 100 characters") + private String codeType; // MODEL, NOTEBOOK, REPOSITORY, HYBRID + + // From ModelResource + @Column + private String applicationInterfaceId; + + @Column + @Size(max = 50, message = "Version must not exceed 50 characters") + private String version; + + // From NotebookResource + @Column + private String notebookPath; + + // From RepositoryResource + @Column + private String repositoryUrl; + + // Combined metadata fields + @Column + @Size(max = 100, message = "Programming language must not exceed 100 characters") + private String programmingLanguage; // Python, R, Java, etc. + + @Column + @Size(max = 100, message = "Framework must not exceed 100 characters") + private String framework; // TensorFlow, PyTorch, Scikit-learn, etc. + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "code_authors", joinColumns = @JoinColumn(name = "code_id")) + @Column(name = "author_email") + private Set authors = new HashSet<>(); + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "code_tags", joinColumns = @JoinColumn(name = "code_id")) + @Column(name = "tag") + private Set tags = new HashSet<>(); + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "code_dependencies", joinColumns = @JoinColumn(name = "code_id")) + @Column(name = "dependency") + private Set dependencies = new HashSet<>(); + + @Column(nullable = false) + private Boolean isPublic = true; + + @Column(nullable = false) + private Boolean isActive = true; + + @Column(nullable = false) + private Integer starCount = 0; + + @Column(columnDefinition = "TEXT") + private String additionalInfo; + + @Column(nullable = false, updatable = false) + @CreatedDate + private Instant createdAt; + + @Column(nullable = false) + @LastModifiedDate + private Instant updatedAt; + + // Default constructor + public Code() {} + + // Main constructor for creating code entities + public Code(String name, String description, String codeType, String programmingLanguage, + String framework, Set authors, Set tags) { + this.name = name; + this.description = description; + this.codeType = codeType; + this.programmingLanguage = programmingLanguage; + this.framework = framework; + this.authors = authors != null ? authors : new HashSet<>(); + this.tags = tags != null ? tags : new HashSet<>(); + this.isPublic = true; + this.isActive = true; + this.starCount = 0; + } + + // Getters and Setters + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getCodeType() { + return codeType; + } + + public void setCodeType(String codeType) { + this.codeType = codeType; + } + + public String getApplicationInterfaceId() { + return applicationInterfaceId; + } + + public void setApplicationInterfaceId(String applicationInterfaceId) { + this.applicationInterfaceId = applicationInterfaceId; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getNotebookPath() { + return notebookPath; + } + + public void setNotebookPath(String notebookPath) { + this.notebookPath = notebookPath; + } + + public String getRepositoryUrl() { + return repositoryUrl; + } + + public void setRepositoryUrl(String repositoryUrl) { + this.repositoryUrl = repositoryUrl; + } + + public String getProgrammingLanguage() { + return programmingLanguage; + } + + public void setProgrammingLanguage(String programmingLanguage) { + this.programmingLanguage = programmingLanguage; + } + + public String getFramework() { + return framework; + } + + public void setFramework(String framework) { + this.framework = framework; + } + + public Set getAuthors() { + return authors; + } + + public void setAuthors(Set authors) { + this.authors = authors; + } + + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + this.tags = tags; + } + + public Set getDependencies() { + return dependencies; + } + + public void setDependencies(Set dependencies) { + this.dependencies = dependencies; + } + + public Boolean getIsPublic() { + return isPublic; + } + + public void setIsPublic(Boolean isPublic) { + this.isPublic = isPublic; + } + + public Boolean getIsActive() { + return isActive; + } + + public void setIsActive(Boolean isActive) { + this.isActive = isActive; + } + + public Integer getStarCount() { + return starCount; + } + + public void setStarCount(Integer starCount) { + this.starCount = starCount; + } + + public String getAdditionalInfo() { + return additionalInfo; + } + + public void setAdditionalInfo(String additionalInfo) { + this.additionalInfo = additionalInfo; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/CodeRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/CodeRepository.java new file mode 100644 index 00000000000..7f54bfa0425 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/CodeRepository.java @@ -0,0 +1,92 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.repository; + +import java.util.List; +import org.apache.airavata.research.service.v2.entity.Code; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface CodeRepository extends JpaRepository { + + // Find by name containing (case insensitive) + List findByNameContainingIgnoreCaseAndIsPublicTrue(String name); + + // Find by code type + List findByCodeTypeAndIsPublicTrue(String codeType); + + // Find by programming language + List findByProgrammingLanguageAndIsPublicTrue(String programmingLanguage); + + // Find by framework + List findByFrameworkAndIsPublicTrue(String framework); + + // Find all public and active codes with pagination + Page findByIsPublicTrueAndIsActiveTrue(Pageable pageable); + + // Search by name, description, or tags with pagination + @Query("SELECT c FROM Code c WHERE c.isPublic = true AND c.isActive = true AND " + + "(LOWER(c.name) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + + "LOWER(c.description) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + + "LOWER(c.codeType) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + + "LOWER(c.programmingLanguage) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + + "LOWER(c.framework) LIKE LOWER(CONCAT('%', :keyword, '%')))") + Page findByKeywordSearchAndIsPublicTrueAndIsActiveTrue(@Param("keyword") String keyword, Pageable pageable); + + // Search codes by keyword (for simple list) + @Query("SELECT c FROM Code c WHERE c.isPublic = true AND c.isActive = true AND " + + "(LOWER(c.name) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + + "LOWER(c.description) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + + "LOWER(c.codeType) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + + "LOWER(c.programmingLanguage) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + + "LOWER(c.framework) LIKE LOWER(CONCAT('%', :keyword, '%')))") + List findByKeywordSearchAndIsPublicTrueAndIsActiveTrue(@Param("keyword") String keyword); + + // Find all public and active codes + List findAllByIsPublicTrueAndIsActiveTrue(); + + // Find codes by tag + @Query("SELECT c FROM Code c JOIN c.tags t WHERE c.isPublic = true AND c.isActive = true AND LOWER(t) = LOWER(:tag)") + List findByTagAndIsPublicTrue(@Param("tag") String tag); + + // Find codes by author + @Query("SELECT c FROM Code c JOIN c.authors a WHERE c.isPublic = true AND c.isActive = true AND LOWER(a) LIKE LOWER(CONCAT('%', :author, '%'))") + List findByAuthorAndIsPublicTrue(@Param("author") String author); + + // Find codes by dependency + @Query("SELECT c FROM Code c JOIN c.dependencies d WHERE c.isPublic = true AND c.isActive = true AND LOWER(d) = LOWER(:dependency)") + List findByDependencyAndIsPublicTrue(@Param("dependency") String dependency); + + // Find top starred codes + @Query("SELECT c FROM Code c WHERE c.isPublic = true AND c.isActive = true ORDER BY c.starCount DESC") + List findTopStarredCodes(Pageable pageable); + + // Find recently created codes + @Query("SELECT c FROM Code c WHERE c.isPublic = true AND c.isActive = true ORDER BY c.createdAt DESC") + List findRecentCodes(Pageable pageable); + + // Find starred codes (starCount > 0) + Page findByStarCountGreaterThanAndIsPublicTrueAndIsActiveTrue(int starCount, Pageable pageable); +} \ No newline at end of file From 45bd5a7de1553911061d9801582ca8c11e1be32d Mon Sep 17 00:00:00 2001 From: Krish Katariya Date: Tue, 29 Jul 2025 09:37:02 -0700 Subject: [PATCH 04/17] Modifying architecture slightly --- .../service/v2/config/V2DataInitializer.java | 300 +++++++++++++----- .../service/v2/controller/CodeController.java | 36 ++- .../controller/ComputeResourceController.java | 20 +- .../controller/StorageResourceController.java | 20 +- .../research/service/v2/entity/Code.java | 154 +-------- .../service/v2/entity/ComputeResource.java | 117 +------ .../service/v2/entity/ResourceV2.java | 204 ++++++++++++ .../service/v2/entity/StorageResource.java | 115 +------ .../research/service/v2/entity/TagV2.java | 57 ++++ .../service/v2/enums/PrivacyEnumV2.java | 25 ++ .../service/v2/enums/ResourceTypeEnumV2.java | 36 +++ .../service/v2/enums/StateEnumV2.java | 25 ++ .../service/v2/enums/StatusEnumV2.java | 27 ++ .../service/v2/repository/CodeRepository.java | 62 ++-- .../repository/ComputeResourceRepository.java | 17 +- .../repository/StorageResourceRepository.java | 17 +- .../v2/repository/TagV2Repository.java | 32 ++ 17 files changed, 777 insertions(+), 487 deletions(-) create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ResourceV2.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/TagV2.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/PrivacyEnumV2.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/ResourceTypeEnumV2.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/StateEnumV2.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/StatusEnumV2.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/TagV2Repository.java diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java index 2e84a66a9c9..7aa8d0a84f5 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java @@ -20,14 +20,21 @@ package org.apache.airavata.research.service.v2.config; import jakarta.annotation.PostConstruct; +import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import org.apache.airavata.research.service.v2.entity.Code; import org.apache.airavata.research.service.v2.entity.ComputeResource; import org.apache.airavata.research.service.v2.entity.StorageResource; +import org.apache.airavata.research.service.v2.entity.TagV2; +import org.apache.airavata.research.service.v2.enums.PrivacyEnumV2; +import org.apache.airavata.research.service.v2.enums.StateEnumV2; +import org.apache.airavata.research.service.v2.enums.StatusEnumV2; import org.apache.airavata.research.service.v2.repository.CodeRepository; import org.apache.airavata.research.service.v2.repository.ComputeResourceRepository; import org.apache.airavata.research.service.v2.repository.StorageResourceRepository; +import org.apache.airavata.research.service.v2.repository.TagV2Repository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -40,24 +47,32 @@ public class V2DataInitializer { private final ComputeResourceRepository computeResourceRepository; private final StorageResourceRepository storageResourceRepository; private final CodeRepository codeRepository; + private final TagV2Repository tagV2Repository; public V2DataInitializer(ComputeResourceRepository computeResourceRepository, StorageResourceRepository storageResourceRepository, - CodeRepository codeRepository) { + CodeRepository codeRepository, + TagV2Repository tagV2Repository) { this.computeResourceRepository = computeResourceRepository; this.storageResourceRepository = storageResourceRepository; this.codeRepository = codeRepository; + this.tagV2Repository = tagV2Repository; } @PostConstruct public void initializeData() { LOGGER.info("Initializing V2 mock data for compute, storage, and code resources..."); - initializeComputeResources(); - initializeStorageResources(); - initializeCodes(); - - LOGGER.info("V2 mock data initialization completed."); + try { + initializeComputeResources(); + initializeStorageResources(); + initializeCodes(); + + LOGGER.info("V2 mock data initialization completed."); + } catch (Exception e) { + LOGGER.error("Error during V2 data initialization: {}", e.getMessage(), e); + throw new RuntimeException("Failed to initialize V2 mock data", e); + } } private void initializeComputeResources() { @@ -329,158 +344,283 @@ private void initializeCodes() { if (codeRepository.count() == 0) { LOGGER.info("Creating mock code resources..."); - List codes = List.of( + // Sample data configurations + CodeData[] codeDataArray = { // Model-type codes - new Code( + new CodeData( "COVID-19 Chest X-ray Classification Model", "Deep learning model for automatic detection of COVID-19 pneumonia from chest X-ray images using ResNet-50 architecture with transfer learning.", "MODEL", "Python", "TensorFlow", Set.of("dr.sarah.medical@stanford.edu", "alex.vision@mit.edu"), - Set.of("medical", "computer-vision", "covid-19", "deep-learning", "classification") + Set.of("medical", "computer-vision", "covid-19", "deep-learning", "classification"), + "covid_xray_classifier_v2.1", + "2.1", + null, + null, + "Pre-trained model weights available. Compatible with standard ML pipelines." ), - new Code( + new CodeData( "Financial Fraud Detection Model", "Machine learning ensemble model combining XGBoost and Random Forest for real-time credit card fraud detection with 99.2% accuracy.", "MODEL", "Python", "Scikit-learn", Set.of("mike.finance@jpmorgan.com", "lisa.ml@visa.com"), - Set.of("finance", "fraud-detection", "machine-learning", "ensemble", "xgboost") + Set.of("finance", "fraud-detection", "machine-learning", "ensemble", "xgboost"), + "fraud_detector_ensemble_v1.3", + "1.3", + null, + null, + "Pre-trained model weights available. Compatible with standard ML pipelines." ), - new Code( + new CodeData( "Protein Folding Prediction Model", "AlphaFold2-inspired neural network for predicting 3D protein structures from amino acid sequences using attention mechanisms.", "MODEL", "Python", "PyTorch", Set.of("prof.chen@deepmind.com", "bio.researcher@harvard.edu"), - Set.of("bioinformatics", "protein-folding", "deep-learning", "attention", "alphafold") + Set.of("bioinformatics", "protein-folding", "deep-learning", "attention", "alphafold"), + "protein_fold_predictor_v3.0", + "3.0", + null, + null, + "Pre-trained model weights available. Compatible with standard ML pipelines." ), // Notebook-type codes - new Code( + new CodeData( "Cybersecurity Threat Analysis Notebook", "Comprehensive Jupyter notebook for analyzing network traffic patterns and identifying potential cybersecurity threats using statistical analysis.", "NOTEBOOK", "Python", null, Set.of("security.analyst@cisco.com", "threat.hunter@crowdstrike.com"), - Set.of("cybersecurity", "threat-analysis", "network-security", "statistical-analysis", "jupyter") + Set.of("cybersecurity", "threat-analysis", "network-security", "statistical-analysis", "jupyter"), + null, + null, + "/notebooks/cybersecurity/threat_analysis.ipynb", + null, + "Interactive Jupyter notebook with step-by-step analysis and visualizations." ), - new Code( + new CodeData( "Climate Data Visualization Notebook", "Interactive data visualization notebook for climate change analysis using NOAA datasets with advanced plotting and statistical modeling.", "NOTEBOOK", "Python", null, Set.of("climate.scientist@noaa.gov", "data.viz@nasa.gov"), - Set.of("climate-science", "data-visualization", "environmental", "statistical-modeling", "matplotlib") + Set.of("climate-science", "data-visualization", "environmental", "statistical-modeling", "matplotlib"), + null, + null, + "/notebooks/climate/climate_visualization.ipynb", + null, + "Interactive Jupyter notebook with step-by-step analysis and visualizations." ), - new Code( + new CodeData( "NLP Sentiment Analysis Notebook", "End-to-end natural language processing pipeline for sentiment analysis of social media data using transformer models and BERT.", "NOTEBOOK", "Python", null, Set.of("nlp.researcher@google.com", "sentiment.expert@twitter.com"), - Set.of("nlp", "sentiment-analysis", "transformers", "bert", "social-media") + Set.of("nlp", "sentiment-analysis", "transformers", "bert", "social-media"), + null, + null, + "/notebooks/nlp/sentiment_analysis.ipynb", + null, + "Interactive Jupyter notebook with step-by-step analysis and visualizations." ), // Repository-type codes - new Code( + new CodeData( "Distributed Machine Learning Framework", "Open-source framework for distributed machine learning across multiple compute nodes with fault tolerance and auto-scaling capabilities.", "REPOSITORY", "Python", "PyTorch", Set.of("distributed.ml@uber.com", "framework.dev@netflix.com"), - Set.of("distributed-computing", "machine-learning", "framework", "pytorch", "scalability") + Set.of("distributed-computing", "machine-learning", "framework", "pytorch", "scalability"), + null, + null, + null, + "https://github.com/ml-distributed/framework", + "Full source code repository with documentation, tests, and CI/CD pipeline." ), - new Code( + new CodeData( "Quantum Computing Algorithms Library", "Comprehensive library of quantum computing algorithms implemented in Qiskit with educational examples and benchmarking tools.", "REPOSITORY", "Python", "Qiskit", Set.of("quantum.researcher@ibm.com", "algorithms.expert@google.com"), - Set.of("quantum-computing", "algorithms", "qiskit", "benchmarking", "education") + Set.of("quantum-computing", "algorithms", "qiskit", "benchmarking", "education"), + null, + null, + null, + "https://github.com/quantum-algorithms/qiskit-library", + "Full source code repository with documentation, tests, and CI/CD pipeline." ), - new Code( + new CodeData( "Time Series Forecasting Toolkit", "Advanced time series analysis and forecasting toolkit with support for ARIMA, LSTM, and Prophet models for financial and IoT data.", "REPOSITORY", "Python", "TensorFlow", Set.of("time.series@bloomberg.com", "forecasting.expert@amazon.com"), - Set.of("time-series", "forecasting", "arima", "lstm", "prophet", "financial") + Set.of("time-series", "forecasting", "arima", "lstm", "prophet", "financial"), + null, + null, + null, + "https://github.com/timeseries-toolkit/forecasting", + "Full source code repository with documentation, tests, and CI/CD pipeline." ) - ); + }; - // Set additional properties for codes - for (int i = 0; i < codes.size(); i++) { - Code code = codes.get(i); - - // Add some random star counts for demonstration - int starCount = (int) (Math.random() * 1000) + 10; - code.setStarCount(starCount); - - // Set type-specific fields - if ("MODEL".equals(code.getCodeType())) { - if (i == 0) { // COVID model - code.setApplicationInterfaceId("covid_xray_classifier_v2.1"); - code.setVersion("2.1"); - } else if (i == 1) { // Fraud model - code.setApplicationInterfaceId("fraud_detector_ensemble_v1.3"); - code.setVersion("1.3"); - } else if (i == 2) { // Protein model - code.setApplicationInterfaceId("protein_fold_predictor_v3.0"); - code.setVersion("3.0"); - } - code.setAdditionalInfo("Pre-trained model weights available. Compatible with standard ML pipelines."); - } else if ("NOTEBOOK".equals(code.getCodeType())) { - if (i == 3) { // Cybersecurity notebook - code.setNotebookPath("/notebooks/cybersecurity/threat_analysis.ipynb"); - } else if (i == 4) { // Climate notebook - code.setNotebookPath("/notebooks/climate/climate_visualization.ipynb"); - } else if (i == 5) { // NLP notebook - code.setNotebookPath("/notebooks/nlp/sentiment_analysis.ipynb"); - } - code.setAdditionalInfo("Interactive Jupyter notebook with step-by-step analysis and visualizations."); - } else if ("REPOSITORY".equals(code.getCodeType())) { - if (i == 6) { // ML Framework - code.setRepositoryUrl("https://github.com/ml-distributed/framework"); - } else if (i == 7) { // Quantum library - code.setRepositoryUrl("https://github.com/quantum-algorithms/qiskit-library"); - } else if (i == 8) { // Time series toolkit - code.setRepositoryUrl("https://github.com/timeseries-toolkit/forecasting"); - } - code.setAdditionalInfo("Full source code repository with documentation, tests, and CI/CD pipeline."); - } - - // Add some dependencies based on programming language and framework - if ("Python".equals(code.getProgrammingLanguage())) { - code.getDependencies().addAll(Set.of("numpy", "pandas", "matplotlib")); - - if ("TensorFlow".equals(code.getFramework())) { - code.getDependencies().addAll(Set.of("tensorflow>=2.8.0", "keras")); - } else if ("PyTorch".equals(code.getFramework())) { - code.getDependencies().addAll(Set.of("torch>=1.12.0", "torchvision")); - } else if ("Scikit-learn".equals(code.getFramework())) { - code.getDependencies().addAll(Set.of("scikit-learn>=1.1.0", "joblib")); - } + for (CodeData codeData : codeDataArray) { + try { + Code code = createCodeWithTags(codeData); + codeRepository.save(code); + } catch (Exception e) { + LOGGER.error("Error creating code '{}': {}", codeData.name, e.getMessage(), e); } } - codeRepository.saveAll(codes); - LOGGER.info("Created {} code resources", codes.size()); + LOGGER.info("Created {} code resources", codeDataArray.length); + } + } + + /** + * Helper method to create Code entity with proper TagV2 associations + */ + private Code createCodeWithTags(CodeData codeData) { + // Create or get existing TagV2 entities + Set tagEntities = getOrCreateTags(codeData.tagStrings); + + // Create Code entity using the constructor + Code code = new Code(codeData.name, codeData.description, codeData.codeType, + codeData.programmingLanguage, codeData.framework, + codeData.authors, tagEntities); + + // Set enum-based fields with proper defaults + code.setStatus(StatusEnumV2.VERIFIED); + code.setState(StateEnumV2.ACTIVE); + code.setPrivacy(PrivacyEnumV2.PUBLIC); + + // Set random star count for demonstration + int starCount = (int) (Math.random() * 1000) + 10; + code.setStarCount(starCount); + + // Set code-specific fields + if (codeData.applicationInterfaceId != null) { + code.setApplicationInterfaceId(codeData.applicationInterfaceId); + } + if (codeData.version != null) { + code.setVersion(codeData.version); + } + if (codeData.notebookPath != null) { + code.setNotebookPath(codeData.notebookPath); + } + if (codeData.repositoryUrl != null) { + code.setRepositoryUrl(codeData.repositoryUrl); + } + if (codeData.additionalInfo != null) { + code.setAdditionalInfo(codeData.additionalInfo); + } + + // Set header image - use a default for now + code.setHeaderImage("https://via.placeholder.com/400x200?text=" + codeData.codeType); + + // Add dependencies based on programming language and framework + addDependencies(code); + + return code; + } + + /** + * Helper method to get or create TagV2 entities from tag strings + */ + private Set getOrCreateTags(Set tagStrings) { + Set tagEntities = new HashSet<>(); + + for (String tagString : tagStrings) { + Optional existingTag = tagV2Repository.findByTagValue(tagString); + if (existingTag.isPresent()) { + tagEntities.add(existingTag.get()); + } else { + // Create new tag + TagV2 newTag = new TagV2(); + newTag.setTagValue(tagString); + TagV2 savedTag = tagV2Repository.save(newTag); + tagEntities.add(savedTag); + LOGGER.debug("Created new tag: {}", tagString); + } + } + + return tagEntities; + } + + /** + * Helper method to add dependencies based on programming language and framework + */ + private void addDependencies(Code code) { + if ("Python".equals(code.getProgrammingLanguage())) { + code.getDependencies().addAll(Set.of("numpy", "pandas", "matplotlib")); + + String framework = code.getFramework(); + if ("TensorFlow".equals(framework)) { + code.getDependencies().addAll(Set.of("tensorflow>=2.8.0", "keras")); + } else if ("PyTorch".equals(framework)) { + code.getDependencies().addAll(Set.of("torch>=1.12.0", "torchvision")); + } else if ("Scikit-learn".equals(framework)) { + code.getDependencies().addAll(Set.of("scikit-learn>=1.1.0", "joblib")); + } else if ("Qiskit".equals(framework)) { + code.getDependencies().addAll(Set.of("qiskit>=0.39.0", "qiskit-aer")); + } + } + } + + /** + * Data structure to hold code initialization data + */ + private static class CodeData { + final String name; + final String description; + final String codeType; + final String programmingLanguage; + final String framework; + final Set authors; + final Set tagStrings; + final String applicationInterfaceId; + final String version; + final String notebookPath; + final String repositoryUrl; + final String additionalInfo; + + CodeData(String name, String description, String codeType, String programmingLanguage, + String framework, Set authors, Set tagStrings, + String applicationInterfaceId, String version, String notebookPath, + String repositoryUrl, String additionalInfo) { + this.name = name; + this.description = description; + this.codeType = codeType; + this.programmingLanguage = programmingLanguage; + this.framework = framework; + this.authors = authors != null ? authors : new HashSet<>(); + this.tagStrings = tagStrings != null ? tagStrings : new HashSet<>(); + this.applicationInterfaceId = applicationInterfaceId; + this.version = version; + this.notebookPath = notebookPath; + this.repositoryUrl = repositoryUrl; + this.additionalInfo = additionalInfo; } } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java index 047d731d652..b1489469ca2 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java @@ -25,6 +25,8 @@ import java.util.Optional; import jakarta.validation.Valid; import org.apache.airavata.research.service.v2.entity.Code; +import org.apache.airavata.research.service.v2.enums.PrivacyEnumV2; +import org.apache.airavata.research.service.v2.enums.StateEnumV2; import org.apache.airavata.research.service.v2.repository.CodeRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,6 +53,8 @@ public class CodeController { private static final Logger LOGGER = LoggerFactory.getLogger(CodeController.class); + private static final PrivacyEnumV2 PUBLIC_PRIVACY = PrivacyEnumV2.PUBLIC; + private static final StateEnumV2 ACTIVE_STATE = StateEnumV2.ACTIVE; private final CodeRepository codeRepository; @@ -74,9 +78,9 @@ public ResponseEntity> getCodes( Page codes; if (keyword != null && !keyword.trim().isEmpty()) { - codes = codeRepository.findByKeywordSearchAndIsPublicTrueAndIsActiveTrue(keyword, pageable); + codes = codeRepository.findByKeywordSearchAndPrivacyAndState(keyword, PUBLIC_PRIVACY, ACTIVE_STATE, pageable); } else { - codes = codeRepository.findByIsPublicTrueAndIsActiveTrue(pageable); + codes = codeRepository.findByPrivacyAndState(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); } LOGGER.info("Found {} codes", codes.getTotalElements()); @@ -113,12 +117,12 @@ public ResponseEntity createCode(@Valid @RequestBody Code code, BindingResult } try { - // Set default values for fields that might be null - if (code.getIsPublic() == null) { - code.setIsPublic(true); + // Set default values using enums + if (code.getPrivacy() == null) { + code.setPrivacy(PUBLIC_PRIVACY); } - if (code.getIsActive() == null) { - code.setIsActive(true); + if (code.getState() == null) { + code.setState(ACTIVE_STATE); } if (code.getStarCount() == null) { code.setStarCount(0); @@ -206,7 +210,7 @@ public ResponseEntity> searchCodes( LOGGER.info("Searching codes with keyword: {}", keyword); - List codes = codeRepository.findByKeywordSearchAndIsPublicTrueAndIsActiveTrue(keyword); + List codes = codeRepository.findByKeywordSearchAndPrivacyAndState(keyword, PUBLIC_PRIVACY, ACTIVE_STATE); LOGGER.info("Found {} codes matching keyword: {}", codes.size(), keyword); return ResponseEntity.ok(codes); @@ -219,7 +223,7 @@ public ResponseEntity> getCodesByType( LOGGER.info("Getting codes by type: {}", codeType); - List codes = codeRepository.findByCodeTypeAndIsPublicTrue(codeType); + List codes = codeRepository.findByCodeTypeAndPrivacyAndState(codeType, PUBLIC_PRIVACY, ACTIVE_STATE); LOGGER.info("Found {} codes of type: {}", codes.size(), codeType); return ResponseEntity.ok(codes); @@ -232,7 +236,7 @@ public ResponseEntity> getCodesByLanguage( LOGGER.info("Getting codes by programming language: {}", programmingLanguage); - List codes = codeRepository.findByProgrammingLanguageAndIsPublicTrue(programmingLanguage); + List codes = codeRepository.findByProgrammingLanguageAndPrivacyAndState(programmingLanguage, PUBLIC_PRIVACY, ACTIVE_STATE); LOGGER.info("Found {} codes for language: {}", codes.size(), programmingLanguage); return ResponseEntity.ok(codes); @@ -245,7 +249,7 @@ public ResponseEntity> getCodesByFramework( LOGGER.info("Getting codes by framework: {}", framework); - List codes = codeRepository.findByFrameworkAndIsPublicTrue(framework); + List codes = codeRepository.findByFrameworkAndPrivacyAndState(framework, PUBLIC_PRIVACY, ACTIVE_STATE); LOGGER.info("Found {} codes for framework: {}", codes.size(), framework); return ResponseEntity.ok(codes); @@ -258,7 +262,7 @@ public ResponseEntity> getCodesByTag( LOGGER.info("Getting codes by tag: {}", tag); - List codes = codeRepository.findByTagAndIsPublicTrue(tag); + List codes = codeRepository.findByTagAndPrivacyAndState(tag, PUBLIC_PRIVACY, ACTIVE_STATE); LOGGER.info("Found {} codes with tag: {}", codes.size(), tag); return ResponseEntity.ok(codes); @@ -271,7 +275,7 @@ public ResponseEntity> getCodesByAuthor( LOGGER.info("Getting codes by author: {}", author); - List codes = codeRepository.findByAuthorAndIsPublicTrue(author); + List codes = codeRepository.findByAuthorAndPrivacyAndState(author, PUBLIC_PRIVACY, ACTIVE_STATE); LOGGER.info("Found {} codes by author: {}", codes.size(), author); return ResponseEntity.ok(codes); @@ -285,7 +289,7 @@ public ResponseEntity> getTopStarredCodes( LOGGER.info("Getting top {} starred codes", limit); Pageable pageable = PageRequest.of(0, limit); - List codes = codeRepository.findTopStarredCodes(pageable); + List codes = codeRepository.findTopStarredCodes(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); LOGGER.info("Found {} top starred codes", codes.size()); return ResponseEntity.ok(codes); @@ -299,7 +303,7 @@ public ResponseEntity> getRecentCodes( LOGGER.info("Getting {} recent codes", limit); Pageable pageable = PageRequest.of(0, limit); - List codes = codeRepository.findRecentCodes(pageable); + List codes = codeRepository.findRecentCodes(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); LOGGER.info("Found {} recent codes", codes.size()); return ResponseEntity.ok(codes); @@ -394,7 +398,7 @@ public ResponseEntity> getStarredCodes( try { Pageable pageable = PageRequest.of(page, size); // Get codes where starCount > 0 (i.e., starred codes) - Page starredCodes = codeRepository.findByStarCountGreaterThanAndIsPublicTrueAndIsActiveTrue(0, pageable); + Page starredCodes = codeRepository.findByStarCountGreaterThanAndPrivacyAndState(0, PUBLIC_PRIVACY, ACTIVE_STATE, pageable); LOGGER.info("Found {} starred codes", starredCodes.getTotalElements()); return ResponseEntity.ok(starredCodes); } catch (Exception e) { diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java index 1877a28851b..42890de0943 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java @@ -25,6 +25,8 @@ import java.util.Optional; import jakarta.validation.Valid; import org.apache.airavata.research.service.v2.entity.ComputeResource; +import org.apache.airavata.research.service.v2.enums.PrivacyEnumV2; +import org.apache.airavata.research.service.v2.enums.StateEnumV2; import org.apache.airavata.research.service.v2.repository.ComputeResourceRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,6 +53,8 @@ public class ComputeResourceController { private static final Logger LOGGER = LoggerFactory.getLogger(ComputeResourceController.class); + private static final PrivacyEnumV2 PUBLIC_PRIVACY = PrivacyEnumV2.PUBLIC; + private static final StateEnumV2 ACTIVE_STATE = StateEnumV2.ACTIVE; private final ComputeResourceRepository computeResourceRepository; @@ -72,9 +76,9 @@ public ResponseEntity> getComputeResources( Page resources; if (nameSearch != null && !nameSearch.trim().isEmpty()) { - resources = computeResourceRepository.findByNameSearchAndIsPublicTrueAndIsActiveTrue(nameSearch, pageable); + resources = computeResourceRepository.findByNameSearchAndPrivacyAndState(nameSearch, PUBLIC_PRIVACY, ACTIVE_STATE, pageable); } else { - resources = computeResourceRepository.findByIsPublicTrueAndIsActiveTrue(pageable); + resources = computeResourceRepository.findByPrivacyAndState(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); } LOGGER.info("Found {} compute resources", resources.getTotalElements()); @@ -118,11 +122,11 @@ public ResponseEntity createComputeResource(@Valid @RequestBody ComputeResour if (computeResource.getMemoryGB() == null) { computeResource.setMemoryGB(1); // Default to 1 GB } - if (computeResource.getIsPublic() == null) { - computeResource.setIsPublic(true); + if (computeResource.getPrivacy() == null) { + computeResource.setPrivacy(PUBLIC_PRIVACY); } - if (computeResource.getIsActive() == null) { - computeResource.setIsActive(true); + if (computeResource.getState() == null) { + computeResource.setState(ACTIVE_STATE); } ComputeResource savedResource = computeResourceRepository.save(computeResource); @@ -205,7 +209,7 @@ public ResponseEntity> searchComputeResources( LOGGER.info("Searching compute resources with keyword: {}", keyword); List resources = computeResourceRepository - .findByNameContainingIgnoreCaseAndIsPublicTrue(keyword); + .findByNameContainingIgnoreCaseAndPrivacyAndState(keyword, PUBLIC_PRIVACY, ACTIVE_STATE); LOGGER.info("Found {} compute resources matching keyword: {}", resources.size(), keyword); return ResponseEntity.ok(resources); @@ -219,7 +223,7 @@ public ResponseEntity> getComputeResourcesByType( LOGGER.info("Getting compute resources by type: {}", computeType); List resources = computeResourceRepository - .findByComputeTypeAndIsPublicTrue(computeType); + .findByComputeTypeAndPrivacyAndState(computeType, PUBLIC_PRIVACY, ACTIVE_STATE); LOGGER.info("Found {} compute resources of type: {}", resources.size(), computeType); return ResponseEntity.ok(resources); diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java index 06b4b88d9f0..9ec29fc3547 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java @@ -25,6 +25,8 @@ import java.util.Optional; import jakarta.validation.Valid; import org.apache.airavata.research.service.v2.entity.StorageResource; +import org.apache.airavata.research.service.v2.enums.PrivacyEnumV2; +import org.apache.airavata.research.service.v2.enums.StateEnumV2; import org.apache.airavata.research.service.v2.repository.StorageResourceRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,6 +53,8 @@ public class StorageResourceController { private static final Logger LOGGER = LoggerFactory.getLogger(StorageResourceController.class); + private static final PrivacyEnumV2 PUBLIC_PRIVACY = PrivacyEnumV2.PUBLIC; + private static final StateEnumV2 ACTIVE_STATE = StateEnumV2.ACTIVE; private final StorageResourceRepository storageResourceRepository; @@ -72,9 +76,9 @@ public ResponseEntity> getStorageResources( Page resources; if (nameSearch != null && !nameSearch.trim().isEmpty()) { - resources = storageResourceRepository.findByNameSearchAndIsPublicTrueAndIsActiveTrue(nameSearch, pageable); + resources = storageResourceRepository.findByNameSearchAndPrivacyAndState(nameSearch, PUBLIC_PRIVACY, ACTIVE_STATE, pageable); } else { - resources = storageResourceRepository.findByIsPublicTrueAndIsActiveTrue(pageable); + resources = storageResourceRepository.findByPrivacyAndState(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); } LOGGER.info("Found {} storage resources", resources.getTotalElements()); @@ -121,11 +125,11 @@ public ResponseEntity createStorageResource(@Valid @RequestBody StorageResour if (storageResource.getSupportsVersioning() == null) { storageResource.setSupportsVersioning(false); } - if (storageResource.getIsPublic() == null) { - storageResource.setIsPublic(true); + if (storageResource.getPrivacy() == null) { + storageResource.setPrivacy(PUBLIC_PRIVACY); } - if (storageResource.getIsActive() == null) { - storageResource.setIsActive(true); + if (storageResource.getState() == null) { + storageResource.setState(ACTIVE_STATE); } StorageResource savedResource = storageResourceRepository.save(storageResource); @@ -208,7 +212,7 @@ public ResponseEntity> searchStorageResources( LOGGER.info("Searching storage resources with keyword: {}", keyword); List resources = storageResourceRepository - .findByNameContainingIgnoreCaseAndIsPublicTrue(keyword); + .findByNameContainingIgnoreCaseAndPrivacyAndState(keyword, PUBLIC_PRIVACY, ACTIVE_STATE); LOGGER.info("Found {} storage resources matching keyword: {}", resources.size(), keyword); return ResponseEntity.ok(resources); @@ -222,7 +226,7 @@ public ResponseEntity> getStorageResourcesByType( LOGGER.info("Getting storage resources by type: {}", storageType); List resources = storageResourceRepository - .findByStorageTypeAndIsPublicTrue(storageType); + .findByStorageTypeAndPrivacyAndState(storageType, PUBLIC_PRIVACY, ACTIVE_STATE); LOGGER.info("Found {} storage resources of type: {}", resources.size(), storageType); return ResponseEntity.ok(resources); diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java index fd52263336d..d6f543e547a 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java @@ -23,42 +23,18 @@ import jakarta.persistence.Column; import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.Table; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; -import java.time.Instant; import java.util.HashSet; import java.util.Set; -import org.hibernate.annotations.UuidGenerator; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.apache.airavata.research.service.v2.enums.ResourceTypeEnumV2; @Entity @Table(name = "CODE_V2") -@EntityListeners(AuditingEntityListener.class) -public class Code { - - @Id - @GeneratedValue - @UuidGenerator - @Column(nullable = false, updatable = false, length = 48) - private String id; - - @Column(nullable = false) - @NotBlank(message = "Name is required") - @Size(max = 255, message = "Name must not exceed 255 characters") - private String name; - - @Column(nullable = false, columnDefinition = "TEXT") - @NotBlank(message = "Description is required") - @Size(max = 5000, message = "Description must not exceed 5000 characters") - private String description; +public class Code extends ResourceV2 { @Column(nullable = false) @NotBlank(message = "Code type is required") @@ -90,84 +66,36 @@ public class Code { @Size(max = 100, message = "Framework must not exceed 100 characters") private String framework; // TensorFlow, PyTorch, Scikit-learn, etc. - @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "code_authors", joinColumns = @JoinColumn(name = "code_id")) - @Column(name = "author_email") - private Set authors = new HashSet<>(); - - @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "code_tags", joinColumns = @JoinColumn(name = "code_id")) - @Column(name = "tag") - private Set tags = new HashSet<>(); - @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "code_dependencies", joinColumns = @JoinColumn(name = "code_id")) @Column(name = "dependency") private Set dependencies = new HashSet<>(); - @Column(nullable = false) - private Boolean isPublic = true; - - @Column(nullable = false) - private Boolean isActive = true; - - @Column(nullable = false) - private Integer starCount = 0; - @Column(columnDefinition = "TEXT") private String additionalInfo; - @Column(nullable = false, updatable = false) - @CreatedDate - private Instant createdAt; - - @Column(nullable = false) - @LastModifiedDate - private Instant updatedAt; + @Override + public ResourceTypeEnumV2 getType() { + return ResourceTypeEnumV2.CODE; + } // Default constructor public Code() {} // Main constructor for creating code entities public Code(String name, String description, String codeType, String programmingLanguage, - String framework, Set authors, Set tags) { - this.name = name; - this.description = description; + String framework, Set authors, Set tags) { + this.setName(name); + this.setDescription(description); this.codeType = codeType; this.programmingLanguage = programmingLanguage; this.framework = framework; - this.authors = authors != null ? authors : new HashSet<>(); - this.tags = tags != null ? tags : new HashSet<>(); - this.isPublic = true; - this.isActive = true; - this.starCount = 0; - } - - // Getters and Setters - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; + this.setAuthors(authors != null ? authors : new HashSet<>()); + this.setTags(tags != null ? tags : new HashSet<>()); + this.setHeaderImage(""); // Default empty header image } + // Getters and Setters for Code-specific fields public String getCodeType() { return codeType; } @@ -224,22 +152,6 @@ public void setFramework(String framework) { this.framework = framework; } - public Set getAuthors() { - return authors; - } - - public void setAuthors(Set authors) { - this.authors = authors; - } - - public Set getTags() { - return tags; - } - - public void setTags(Set tags) { - this.tags = tags; - } - public Set getDependencies() { return dependencies; } @@ -248,30 +160,6 @@ public void setDependencies(Set dependencies) { this.dependencies = dependencies; } - public Boolean getIsPublic() { - return isPublic; - } - - public void setIsPublic(Boolean isPublic) { - this.isPublic = isPublic; - } - - public Boolean getIsActive() { - return isActive; - } - - public void setIsActive(Boolean isActive) { - this.isActive = isActive; - } - - public Integer getStarCount() { - return starCount; - } - - public void setStarCount(Integer starCount) { - this.starCount = starCount; - } - public String getAdditionalInfo() { return additionalInfo; } @@ -279,20 +167,4 @@ public String getAdditionalInfo() { public void setAdditionalInfo(String additionalInfo) { this.additionalInfo = additionalInfo; } - - public Instant getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(Instant createdAt) { - this.createdAt = createdAt; - } - - public Instant getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(Instant updatedAt) { - this.updatedAt = updatedAt; - } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java index 0ffa38feae7..da4214ce54c 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java @@ -21,41 +21,18 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; -import java.time.Instant; -import java.util.List; -import org.hibernate.annotations.UuidGenerator; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import java.util.HashSet; +import java.util.Set; +import org.apache.airavata.research.service.v2.enums.ResourceTypeEnumV2; @Entity @Table(name = "COMPUTE_RESOURCE_V2") -@EntityListeners(AuditingEntityListener.class) -public class ComputeResource { - - @Id - @GeneratedValue - @UuidGenerator - @Column(nullable = false, updatable = false, length = 48) - private String id; - - @Column(nullable = false) - @NotBlank(message = "Name is required") - @Size(max = 255, message = "Name must not exceed 255 characters") - private String name; - - @Column(nullable = false, columnDefinition = "TEXT") - @NotBlank(message = "Description is required") - @Size(max = 5000, message = "Description must not exceed 5000 characters") - private String description; +public class ComputeResource extends ResourceV2 { @Column(nullable = false) @NotBlank(message = "Hostname is required") @@ -90,24 +67,15 @@ public class ComputeResource { @Column(columnDefinition = "TEXT") private String additionalInfo; - @Column(nullable = false) - private Boolean isPublic = true; - - @Column(nullable = false) - private Boolean isActive = true; - @Column(nullable = false) @NotBlank(message = "Resource manager is required") @Size(max = 255, message = "Resource manager must not exceed 255 characters") private String resourceManager; // Gateway name or organization - @Column(nullable = false, updatable = false) - @CreatedDate - private Instant createdAt; - - @Column(nullable = false) - @LastModifiedDate - private Instant updatedAt; + @Override + public ResourceTypeEnumV2 getType() { + return ResourceTypeEnumV2.COMPUTE_RESOURCE; + } // Default constructor public ComputeResource() {} @@ -116,8 +84,8 @@ public ComputeResource() {} public ComputeResource(String name, String description, String hostname, String computeType, Integer cpuCores, Integer memoryGB, String operatingSystem, String queueSystem, String additionalInfo, String resourceManager) { - this.name = name; - this.description = description; + this.setName(name); + this.setDescription(description); this.hostname = hostname; this.computeType = computeType; this.cpuCores = cpuCores; @@ -126,35 +94,14 @@ public ComputeResource(String name, String description, String hostname, String this.queueSystem = queueSystem; this.additionalInfo = additionalInfo; this.resourceManager = resourceManager; - this.isPublic = true; - this.isActive = true; - } - - // Getters and Setters - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; + + // Set inherited fields + this.setAuthors(new HashSet<>()); + this.setTags(new HashSet<>()); + this.setHeaderImage(""); // Default empty header image } + // Getters and Setters for ComputeResource-specific fields public String getHostname() { return hostname; } @@ -211,22 +158,6 @@ public void setAdditionalInfo(String additionalInfo) { this.additionalInfo = additionalInfo; } - public Boolean getIsPublic() { - return isPublic; - } - - public void setIsPublic(Boolean isPublic) { - this.isPublic = isPublic; - } - - public Boolean getIsActive() { - return isActive; - } - - public void setIsActive(Boolean isActive) { - this.isActive = isActive; - } - public String getResourceManager() { return resourceManager; } @@ -234,20 +165,4 @@ public String getResourceManager() { public void setResourceManager(String resourceManager) { this.resourceManager = resourceManager; } - - public Instant getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(Instant createdAt) { - this.createdAt = createdAt; - } - - public Instant getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(Instant updatedAt) { - this.updatedAt = updatedAt; - } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ResourceV2.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ResourceV2.java new file mode 100644 index 00000000000..839802a9aef --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ResourceV2.java @@ -0,0 +1,204 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.entity; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.Table; +import java.time.Instant; +import java.util.HashSet; +import java.util.Set; +import org.apache.airavata.research.service.v2.enums.PrivacyEnumV2; +import org.apache.airavata.research.service.v2.enums.ResourceTypeEnumV2; +import org.apache.airavata.research.service.v2.enums.StateEnumV2; +import org.apache.airavata.research.service.v2.enums.StatusEnumV2; +import org.hibernate.annotations.UuidGenerator; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Entity +@Table(name = "RESOURCE_V2") +@Inheritance(strategy = InheritanceType.JOINED) +@EntityListeners(AuditingEntityListener.class) +public abstract class ResourceV2 { + + @Id + @GeneratedValue + @UuidGenerator + @Column(nullable = false, updatable = false, length = 48) + private String id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false, columnDefinition = "TEXT") + private String description; + + @Column(nullable = false) + private String headerImage; + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "resource_v2_authors", joinColumns = @JoinColumn(name = "resource_id")) + @Column(name = "author_id") + private Set authors = new HashSet<>(); + + @ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.EAGER) + @JoinTable( + name = "resource_v2_tags", + joinColumns = @JoinColumn(name = "resource_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id")) + private Set tags = new HashSet<>(); + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private StatusEnumV2 status = StatusEnumV2.NONE; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private StateEnumV2 state = StateEnumV2.ACTIVE; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private PrivacyEnumV2 privacy = PrivacyEnumV2.PUBLIC; + + @Column(nullable = false) + private Integer starCount = 0; + + @Column(nullable = false, updatable = false) + @CreatedDate + private Instant createdAt; + + @Column(nullable = false) + @LastModifiedDate + private Instant updatedAt; + + public abstract ResourceTypeEnumV2 getType(); + + public String getHeaderImage() { + return headerImage; + } + + public void setHeaderImage(String headerImage) { + this.headerImage = headerImage; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Set getAuthors() { + return authors; + } + + public void setAuthors(Set authors) { + this.authors = authors; + } + + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + this.tags = tags; + } + + public StatusEnumV2 getStatus() { + return status; + } + + public void setStatus(StatusEnumV2 status) { + this.status = status; + } + + public StateEnumV2 getState() { + return state; + } + + public void setState(StateEnumV2 state) { + this.state = state; + } + + public PrivacyEnumV2 getPrivacy() { + return privacy; + } + + public void setPrivacy(PrivacyEnumV2 privacy) { + this.privacy = privacy; + } + + public Integer getStarCount() { + return starCount; + } + + public void setStarCount(Integer starCount) { + this.starCount = starCount; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java index b9e05c2bb6f..a62590cc3aa 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java @@ -21,40 +21,17 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; -import java.time.Instant; -import org.hibernate.annotations.UuidGenerator; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import java.util.HashSet; +import org.apache.airavata.research.service.v2.enums.ResourceTypeEnumV2; @Entity @Table(name = "STORAGE_RESOURCE_V2") -@EntityListeners(AuditingEntityListener.class) -public class StorageResource { - - @Id - @GeneratedValue - @UuidGenerator - @Column(nullable = false, updatable = false, length = 48) - private String id; - - @Column(nullable = false) - @NotBlank(message = "Name is required") - @Size(max = 255, message = "Name must not exceed 255 characters") - private String name; - - @Column(nullable = false, columnDefinition = "TEXT") - @NotBlank(message = "Description is required") - @Size(max = 5000, message = "Description must not exceed 5000 characters") - private String description; +public class StorageResource extends ResourceV2 { @Column(nullable = false) @NotBlank(message = "Hostname is required") @@ -90,24 +67,15 @@ public class StorageResource { @Column(columnDefinition = "TEXT") private String additionalInfo; - @Column(nullable = false) - private Boolean isPublic = true; - - @Column(nullable = false) - private Boolean isActive = true; - @Column(nullable = false) @NotBlank(message = "Resource manager is required") @Size(max = 255, message = "Resource manager must not exceed 255 characters") private String resourceManager; // Gateway name or organization - @Column(nullable = false, updatable = false) - @CreatedDate - private Instant createdAt; - - @Column(nullable = false) - @LastModifiedDate - private Instant updatedAt; + @Override + public ResourceTypeEnumV2 getType() { + return ResourceTypeEnumV2.STORAGE_RESOURCE; + } // Default constructor public StorageResource() {} @@ -117,8 +85,8 @@ public StorageResource(String name, String description, String hostname, String Long capacityTB, String accessProtocol, String endpoint, Boolean supportsEncryption, Boolean supportsVersioning, String additionalInfo, String resourceManager) { - this.name = name; - this.description = description; + this.setName(name); + this.setDescription(description); this.hostname = hostname; this.storageType = storageType; this.capacityTB = capacityTB; @@ -128,35 +96,14 @@ public StorageResource(String name, String description, String hostname, String this.supportsVersioning = supportsVersioning; this.additionalInfo = additionalInfo; this.resourceManager = resourceManager; - this.isPublic = true; - this.isActive = true; - } - - // Getters and Setters - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; + + // Set inherited fields + this.setAuthors(new HashSet<>()); + this.setTags(new HashSet<>()); + this.setHeaderImage(""); // Default empty header image } + // Getters and Setters for StorageResource-specific fields public String getHostname() { return hostname; } @@ -221,22 +168,6 @@ public void setAdditionalInfo(String additionalInfo) { this.additionalInfo = additionalInfo; } - public Boolean getIsPublic() { - return isPublic; - } - - public void setIsPublic(Boolean isPublic) { - this.isPublic = isPublic; - } - - public Boolean getIsActive() { - return isActive; - } - - public void setIsActive(Boolean isActive) { - this.isActive = isActive; - } - public String getResourceManager() { return resourceManager; } @@ -244,20 +175,4 @@ public String getResourceManager() { public void setResourceManager(String resourceManager) { this.resourceManager = resourceManager; } - - public Instant getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(Instant createdAt) { - this.createdAt = createdAt; - } - - public Instant getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(Instant updatedAt) { - this.updatedAt = updatedAt; - } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/TagV2.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/TagV2.java new file mode 100644 index 00000000000..439dcfc22bf --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/TagV2.java @@ -0,0 +1,57 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.UuidGenerator; + +@Entity +@Table(name = "TAG_V2") +public class TagV2 { + + @Id + @GeneratedValue + @UuidGenerator + @Column(nullable = false, updatable = false, length = 48) + private String id; + + @Column(nullable = false) + private String tagValue; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTagValue() { + return tagValue; + } + + public void setTagValue(String tagValue) { + this.tagValue = tagValue; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/PrivacyEnumV2.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/PrivacyEnumV2.java new file mode 100644 index 00000000000..bc3fb186ead --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/PrivacyEnumV2.java @@ -0,0 +1,25 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.enums; + +public enum PrivacyEnumV2 { + PUBLIC, + PRIVATE +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/ResourceTypeEnumV2.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/ResourceTypeEnumV2.java new file mode 100644 index 00000000000..931169bd71a --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/ResourceTypeEnumV2.java @@ -0,0 +1,36 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.enums; + +public enum ResourceTypeEnumV2 { + CODE("CODE"), + COMPUTE_RESOURCE("COMPUTE_RESOURCE"), + STORAGE_RESOURCE("STORAGE_RESOURCE"); + + private String str; + + ResourceTypeEnumV2(String str) { + this.str = str; + } + + public String toString() { + return str; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/StateEnumV2.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/StateEnumV2.java new file mode 100644 index 00000000000..84a00621fce --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/StateEnumV2.java @@ -0,0 +1,25 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.enums; + +public enum StateEnumV2 { + ACTIVE, + DELETED +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/StatusEnumV2.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/StatusEnumV2.java new file mode 100644 index 00000000000..0b17da9f227 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/StatusEnumV2.java @@ -0,0 +1,27 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.enums; + +public enum StatusEnumV2 { + NONE, + PENDING, + VERIFIED, + REJECTED +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/CodeRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/CodeRepository.java index 7f54bfa0425..6aa6f92ddef 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/CodeRepository.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/CodeRepository.java @@ -21,6 +21,8 @@ import java.util.List; import org.apache.airavata.research.service.v2.entity.Code; +import org.apache.airavata.research.service.v2.enums.PrivacyEnumV2; +import org.apache.airavata.research.service.v2.enums.StateEnumV2; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -32,61 +34,79 @@ public interface CodeRepository extends JpaRepository { // Find by name containing (case insensitive) - List findByNameContainingIgnoreCaseAndIsPublicTrue(String name); + List findByNameContainingIgnoreCaseAndPrivacyAndState(String name, PrivacyEnumV2 privacy, StateEnumV2 state); // Find by code type - List findByCodeTypeAndIsPublicTrue(String codeType); + List findByCodeTypeAndPrivacyAndState(String codeType, PrivacyEnumV2 privacy, StateEnumV2 state); // Find by programming language - List findByProgrammingLanguageAndIsPublicTrue(String programmingLanguage); + List findByProgrammingLanguageAndPrivacyAndState(String programmingLanguage, PrivacyEnumV2 privacy, StateEnumV2 state); // Find by framework - List findByFrameworkAndIsPublicTrue(String framework); + List findByFrameworkAndPrivacyAndState(String framework, PrivacyEnumV2 privacy, StateEnumV2 state); // Find all public and active codes with pagination - Page findByIsPublicTrueAndIsActiveTrue(Pageable pageable); + Page findByPrivacyAndState(PrivacyEnumV2 privacy, StateEnumV2 state, Pageable pageable); // Search by name, description, or tags with pagination - @Query("SELECT c FROM Code c WHERE c.isPublic = true AND c.isActive = true AND " + + @Query("SELECT c FROM Code c WHERE c.privacy = :privacy AND c.state = :state AND " + "(LOWER(c.name) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + "LOWER(c.description) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + "LOWER(c.codeType) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + "LOWER(c.programmingLanguage) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + "LOWER(c.framework) LIKE LOWER(CONCAT('%', :keyword, '%')))") - Page findByKeywordSearchAndIsPublicTrueAndIsActiveTrue(@Param("keyword") String keyword, Pageable pageable); + Page findByKeywordSearchAndPrivacyAndState(@Param("keyword") String keyword, + @Param("privacy") PrivacyEnumV2 privacy, + @Param("state") StateEnumV2 state, + Pageable pageable); // Search codes by keyword (for simple list) - @Query("SELECT c FROM Code c WHERE c.isPublic = true AND c.isActive = true AND " + + @Query("SELECT c FROM Code c WHERE c.privacy = :privacy AND c.state = :state AND " + "(LOWER(c.name) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + "LOWER(c.description) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + "LOWER(c.codeType) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + "LOWER(c.programmingLanguage) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + "LOWER(c.framework) LIKE LOWER(CONCAT('%', :keyword, '%')))") - List findByKeywordSearchAndIsPublicTrueAndIsActiveTrue(@Param("keyword") String keyword); + List findByKeywordSearchAndPrivacyAndState(@Param("keyword") String keyword, + @Param("privacy") PrivacyEnumV2 privacy, + @Param("state") StateEnumV2 state); // Find all public and active codes - List findAllByIsPublicTrueAndIsActiveTrue(); + List findAllByPrivacyAndState(PrivacyEnumV2 privacy, StateEnumV2 state); // Find codes by tag - @Query("SELECT c FROM Code c JOIN c.tags t WHERE c.isPublic = true AND c.isActive = true AND LOWER(t) = LOWER(:tag)") - List findByTagAndIsPublicTrue(@Param("tag") String tag); + @Query("SELECT c FROM Code c JOIN c.tags t WHERE c.privacy = :privacy AND c.state = :state AND LOWER(t.tagValue) = LOWER(:tag)") + List findByTagAndPrivacyAndState(@Param("tag") String tag, + @Param("privacy") PrivacyEnumV2 privacy, + @Param("state") StateEnumV2 state); // Find codes by author - @Query("SELECT c FROM Code c JOIN c.authors a WHERE c.isPublic = true AND c.isActive = true AND LOWER(a) LIKE LOWER(CONCAT('%', :author, '%'))") - List findByAuthorAndIsPublicTrue(@Param("author") String author); + @Query("SELECT c FROM Code c JOIN c.authors a WHERE c.privacy = :privacy AND c.state = :state AND LOWER(a) LIKE LOWER(CONCAT('%', :author, '%'))") + List findByAuthorAndPrivacyAndState(@Param("author") String author, + @Param("privacy") PrivacyEnumV2 privacy, + @Param("state") StateEnumV2 state); // Find codes by dependency - @Query("SELECT c FROM Code c JOIN c.dependencies d WHERE c.isPublic = true AND c.isActive = true AND LOWER(d) = LOWER(:dependency)") - List findByDependencyAndIsPublicTrue(@Param("dependency") String dependency); + @Query("SELECT c FROM Code c JOIN c.dependencies d WHERE c.privacy = :privacy AND c.state = :state AND LOWER(d) = LOWER(:dependency)") + List findByDependencyAndPrivacyAndState(@Param("dependency") String dependency, + @Param("privacy") PrivacyEnumV2 privacy, + @Param("state") StateEnumV2 state); // Find top starred codes - @Query("SELECT c FROM Code c WHERE c.isPublic = true AND c.isActive = true ORDER BY c.starCount DESC") - List findTopStarredCodes(Pageable pageable); + @Query("SELECT c FROM Code c WHERE c.privacy = :privacy AND c.state = :state ORDER BY c.starCount DESC") + List findTopStarredCodes(@Param("privacy") PrivacyEnumV2 privacy, + @Param("state") StateEnumV2 state, + Pageable pageable); // Find recently created codes - @Query("SELECT c FROM Code c WHERE c.isPublic = true AND c.isActive = true ORDER BY c.createdAt DESC") - List findRecentCodes(Pageable pageable); + @Query("SELECT c FROM Code c WHERE c.privacy = :privacy AND c.state = :state ORDER BY c.createdAt DESC") + List findRecentCodes(@Param("privacy") PrivacyEnumV2 privacy, + @Param("state") StateEnumV2 state, + Pageable pageable); // Find starred codes (starCount > 0) - Page findByStarCountGreaterThanAndIsPublicTrueAndIsActiveTrue(int starCount, Pageable pageable); + Page findByStarCountGreaterThanAndPrivacyAndState(int starCount, + PrivacyEnumV2 privacy, + StateEnumV2 state, + Pageable pageable); } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java index 8cea8069de9..ce5e65e4746 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java @@ -21,6 +21,8 @@ import java.util.List; import org.apache.airavata.research.service.v2.entity.ComputeResource; +import org.apache.airavata.research.service.v2.enums.PrivacyEnumV2; +import org.apache.airavata.research.service.v2.enums.StateEnumV2; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -32,21 +34,24 @@ public interface ComputeResourceRepository extends JpaRepository { // Find by name containing (case insensitive) - List findByNameContainingIgnoreCaseAndIsPublicTrue(String name); + List findByNameContainingIgnoreCaseAndPrivacyAndState(String name, PrivacyEnumV2 privacy, StateEnumV2 state); // Find by compute type - List findByComputeTypeAndIsPublicTrue(String computeType); + List findByComputeTypeAndPrivacyAndState(String computeType, PrivacyEnumV2 privacy, StateEnumV2 state); // Find all public and active resources with pagination - Page findByIsPublicTrueAndIsActiveTrue(Pageable pageable); + Page findByPrivacyAndState(PrivacyEnumV2 privacy, StateEnumV2 state, Pageable pageable); // Search by name with pagination - @Query("SELECT c FROM ComputeResource c WHERE c.isPublic = true AND c.isActive = true AND " + + @Query("SELECT c FROM ComputeResource c WHERE c.privacy = :privacy AND c.state = :state AND " + "(LOWER(c.name) LIKE LOWER(CONCAT('%', :nameSearch, '%')) OR " + "LOWER(c.description) LIKE LOWER(CONCAT('%', :nameSearch, '%')) OR " + "LOWER(c.computeType) LIKE LOWER(CONCAT('%', :nameSearch, '%')))") - Page findByNameSearchAndIsPublicTrueAndIsActiveTrue(@Param("nameSearch") String nameSearch, Pageable pageable); + Page findByNameSearchAndPrivacyAndState(@Param("nameSearch") String nameSearch, + @Param("privacy") PrivacyEnumV2 privacy, + @Param("state") StateEnumV2 state, + Pageable pageable); // Find all public and active resources - List findAllByIsPublicTrueAndIsActiveTrue(); + List findAllByPrivacyAndState(PrivacyEnumV2 privacy, StateEnumV2 state); } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/StorageResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/StorageResourceRepository.java index cf2fd2810b5..6482ae56f6d 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/StorageResourceRepository.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/StorageResourceRepository.java @@ -21,6 +21,8 @@ import java.util.List; import org.apache.airavata.research.service.v2.entity.StorageResource; +import org.apache.airavata.research.service.v2.enums.PrivacyEnumV2; +import org.apache.airavata.research.service.v2.enums.StateEnumV2; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -32,21 +34,24 @@ public interface StorageResourceRepository extends JpaRepository { // Find by name containing (case insensitive) - List findByNameContainingIgnoreCaseAndIsPublicTrue(String name); + List findByNameContainingIgnoreCaseAndPrivacyAndState(String name, PrivacyEnumV2 privacy, StateEnumV2 state); // Find by storage type - List findByStorageTypeAndIsPublicTrue(String storageType); + List findByStorageTypeAndPrivacyAndState(String storageType, PrivacyEnumV2 privacy, StateEnumV2 state); // Find all public and active resources with pagination - Page findByIsPublicTrueAndIsActiveTrue(Pageable pageable); + Page findByPrivacyAndState(PrivacyEnumV2 privacy, StateEnumV2 state, Pageable pageable); // Search by name with pagination - @Query("SELECT s FROM StorageResource s WHERE s.isPublic = true AND s.isActive = true AND " + + @Query("SELECT s FROM StorageResource s WHERE s.privacy = :privacy AND s.state = :state AND " + "(LOWER(s.name) LIKE LOWER(CONCAT('%', :nameSearch, '%')) OR " + "LOWER(s.description) LIKE LOWER(CONCAT('%', :nameSearch, '%')) OR " + "LOWER(s.storageType) LIKE LOWER(CONCAT('%', :nameSearch, '%')))") - Page findByNameSearchAndIsPublicTrueAndIsActiveTrue(@Param("nameSearch") String nameSearch, Pageable pageable); + Page findByNameSearchAndPrivacyAndState(@Param("nameSearch") String nameSearch, + @Param("privacy") PrivacyEnumV2 privacy, + @Param("state") StateEnumV2 state, + Pageable pageable); // Find all public and active resources - List findAllByIsPublicTrueAndIsActiveTrue(); + List findAllByPrivacyAndState(PrivacyEnumV2 privacy, StateEnumV2 state); } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/TagV2Repository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/TagV2Repository.java new file mode 100644 index 00000000000..ae4d32c808d --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/TagV2Repository.java @@ -0,0 +1,32 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.repository; + +import java.util.Optional; +import org.apache.airavata.research.service.v2.entity.TagV2; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TagV2Repository extends JpaRepository { + + // Find tag by tag value + Optional findByTagValue(String tagValue); +} \ No newline at end of file From 4b7f3bbe824161342f438b43c07983f46726bc64 Mon Sep 17 00:00:00 2001 From: Krish Katariya Date: Wed, 30 Jul 2025 23:39:35 -0700 Subject: [PATCH 05/17] Modifying v2 architecture to be more like v1 API architecture --- .../service/enums/ResourceTypeEnum.java | 5 +- .../research/service/model/entity/Tag.java | 2 +- .../service/v2/config/V2DataInitializer.java | 45 ++-- .../service/v2/controller/CodeController.java | 57 ++--- .../controller/ComputeResourceController.java | 91 ++++++-- .../controller/StorageResourceController.java | 91 ++++++-- .../research/service/v2/entity/Code.java | 19 +- .../service/v2/entity/ComputeResource.java | 17 +- .../service/v2/entity/ResourceV2.java | 204 ------------------ .../service/v2/entity/StorageResource.java | 17 +- .../research/service/v2/entity/TagV2.java | 57 ----- .../service/v2/enums/PrivacyEnumV2.java | 25 --- .../service/v2/enums/ResourceTypeEnumV2.java | 36 ---- .../service/v2/enums/StateEnumV2.java | 25 --- .../service/v2/enums/StatusEnumV2.java | 27 --- .../service/v2/repository/CodeRepository.java | 57 +++-- .../repository/ComputeResourceRepository.java | 16 +- .../repository/StorageResourceRepository.java | 16 +- .../v2/repository/TagV2Repository.java | 32 --- 19 files changed, 286 insertions(+), 553 deletions(-) delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ResourceV2.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/TagV2.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/PrivacyEnumV2.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/ResourceTypeEnumV2.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/StateEnumV2.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/StatusEnumV2.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/TagV2Repository.java diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/enums/ResourceTypeEnum.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/enums/ResourceTypeEnum.java index f0054aa308e..2ea4ac1da8f 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/enums/ResourceTypeEnum.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/enums/ResourceTypeEnum.java @@ -23,7 +23,10 @@ public enum ResourceTypeEnum { NOTEBOOK("NOTEBOOK"), DATASET("DATASET"), REPOSITORY("REPOSITORY"), - MODEL("MODEL"); + MODEL("MODEL"), + CODE("CODE"), + COMPUTE_RESOURCE("COMPUTE_RESOURCE"), + STORAGE_RESOURCE("STORAGE_RESOURCE"); private String str; diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Tag.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Tag.java index e7ac271abd8..457f9ceb8f6 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Tag.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Tag.java @@ -36,7 +36,7 @@ public class Tag { @Column(nullable = false, updatable = false, length = 48) private String id; - @Column(nullable = false) + @Column(name = "tag_value", nullable = false) private String value; public String getId() { diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java index 7aa8d0a84f5..7ed0125026e 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java @@ -24,17 +24,17 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import org.apache.airavata.research.service.enums.PrivacyEnum; +import org.apache.airavata.research.service.enums.StateEnum; +import org.apache.airavata.research.service.enums.StatusEnum; +import org.apache.airavata.research.service.model.entity.Tag; +import org.apache.airavata.research.service.model.repo.TagRepository; import org.apache.airavata.research.service.v2.entity.Code; import org.apache.airavata.research.service.v2.entity.ComputeResource; import org.apache.airavata.research.service.v2.entity.StorageResource; -import org.apache.airavata.research.service.v2.entity.TagV2; -import org.apache.airavata.research.service.v2.enums.PrivacyEnumV2; -import org.apache.airavata.research.service.v2.enums.StateEnumV2; -import org.apache.airavata.research.service.v2.enums.StatusEnumV2; import org.apache.airavata.research.service.v2.repository.CodeRepository; import org.apache.airavata.research.service.v2.repository.ComputeResourceRepository; import org.apache.airavata.research.service.v2.repository.StorageResourceRepository; -import org.apache.airavata.research.service.v2.repository.TagV2Repository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -47,16 +47,16 @@ public class V2DataInitializer { private final ComputeResourceRepository computeResourceRepository; private final StorageResourceRepository storageResourceRepository; private final CodeRepository codeRepository; - private final TagV2Repository tagV2Repository; + private final TagRepository tagRepository; public V2DataInitializer(ComputeResourceRepository computeResourceRepository, StorageResourceRepository storageResourceRepository, CodeRepository codeRepository, - TagV2Repository tagV2Repository) { + TagRepository tagRepository) { this.computeResourceRepository = computeResourceRepository; this.storageResourceRepository = storageResourceRepository; this.codeRepository = codeRepository; - this.tagV2Repository = tagV2Repository; + this.tagRepository = tagRepository; } @PostConstruct @@ -499,11 +499,11 @@ private void initializeCodes() { } /** - * Helper method to create Code entity with proper TagV2 associations + * Helper method to create Code entity with proper Tag associations */ private Code createCodeWithTags(CodeData codeData) { - // Create or get existing TagV2 entities - Set tagEntities = getOrCreateTags(codeData.tagStrings); + // Create or get existing Tag entities + Set tagEntities = getOrCreateTags(codeData.tagStrings); // Create Code entity using the constructor Code code = new Code(codeData.name, codeData.description, codeData.codeType, @@ -511,13 +511,14 @@ private Code createCodeWithTags(CodeData codeData) { codeData.authors, tagEntities); // Set enum-based fields with proper defaults - code.setStatus(StatusEnumV2.VERIFIED); - code.setState(StateEnumV2.ACTIVE); - code.setPrivacy(PrivacyEnumV2.PUBLIC); + code.setStatus(StatusEnum.VERIFIED); + code.setState(StateEnum.ACTIVE); + code.setPrivacy(PrivacyEnum.PUBLIC); // Set random star count for demonstration int starCount = (int) (Math.random() * 1000) + 10; - code.setStarCount(starCount); + // Note: starCount functionality handled separately in v1 star system + // code.setStarCount(starCount); // Removed - v2 entities don't have starCount field // Set code-specific fields if (codeData.applicationInterfaceId != null) { @@ -546,20 +547,20 @@ private Code createCodeWithTags(CodeData codeData) { } /** - * Helper method to get or create TagV2 entities from tag strings + * Helper method to get or create Tag entities from tag strings */ - private Set getOrCreateTags(Set tagStrings) { - Set tagEntities = new HashSet<>(); + private Set getOrCreateTags(Set tagStrings) { + Set tagEntities = new HashSet<>(); for (String tagString : tagStrings) { - Optional existingTag = tagV2Repository.findByTagValue(tagString); + Optional existingTag = Optional.ofNullable(tagRepository.findByValue(tagString)); if (existingTag.isPresent()) { tagEntities.add(existingTag.get()); } else { // Create new tag - TagV2 newTag = new TagV2(); - newTag.setTagValue(tagString); - TagV2 savedTag = tagV2Repository.save(newTag); + Tag newTag = new Tag(); + newTag.setValue(tagString); + Tag savedTag = tagRepository.save(newTag); tagEntities.add(savedTag); LOGGER.debug("Created new tag: {}", tagString); } diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java index b1489469ca2..61f7c8c265c 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java @@ -21,12 +21,12 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import java.util.List; import java.util.Optional; -import jakarta.validation.Valid; +import org.apache.airavata.research.service.enums.PrivacyEnum; +import org.apache.airavata.research.service.enums.StateEnum; import org.apache.airavata.research.service.v2.entity.Code; -import org.apache.airavata.research.service.v2.enums.PrivacyEnumV2; -import org.apache.airavata.research.service.v2.enums.StateEnumV2; import org.apache.airavata.research.service.v2.repository.CodeRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,8 +53,8 @@ public class CodeController { private static final Logger LOGGER = LoggerFactory.getLogger(CodeController.class); - private static final PrivacyEnumV2 PUBLIC_PRIVACY = PrivacyEnumV2.PUBLIC; - private static final StateEnumV2 ACTIVE_STATE = StateEnumV2.ACTIVE; + private static final PrivacyEnum PUBLIC_PRIVACY = PrivacyEnum.PUBLIC; + private static final StateEnum ACTIVE_STATE = StateEnum.ACTIVE; private final CodeRepository codeRepository; @@ -124,9 +124,7 @@ public ResponseEntity createCode(@Valid @RequestBody Code code, BindingResult if (code.getState() == null) { code.setState(ACTIVE_STATE); } - if (code.getStarCount() == null) { - code.setStarCount(0); - } + // Note: starCount functionality handled separately in v1 star system Code savedCode = codeRepository.save(code); LOGGER.info("Created code with ID: {}", savedCode.getId()); @@ -164,11 +162,8 @@ public ResponseEntity updateCode(@PathVariable("id") String id, @Valid @Reque // Set the ID to ensure we update the correct code code.setId(id); - // Preserve creation timestamp and star count + // Preserve creation timestamp code.setCreatedAt(existingCode.get().getCreatedAt()); - if (code.getStarCount() == null) { - code.setStarCount(existingCode.get().getStarCount()); - } Code updatedCode = codeRepository.save(code); LOGGER.info("Successfully updated code with ID: {}", id); @@ -319,23 +314,10 @@ public ResponseEntity starCode(@PathVariable("id") String id) { if (codeOpt.isPresent()) { Code code = codeOpt.get(); - // Simple toggle mechanism - if already starred (starCount > 0), unstar it - // For simplicity, we use starCount as a toggle indicator - boolean isCurrentlyStarred = code.getStarCount() > 0; - - if (isCurrentlyStarred) { - // Unstar: set count to 0 - code.setStarCount(0); - codeRepository.save(code); - LOGGER.info("Code unstarred: {}", id); - return ResponseEntity.ok(false); - } else { - // Star: set count to 1 - code.setStarCount(1); - codeRepository.save(code); - LOGGER.info("Code starred: {}", id); - return ResponseEntity.ok(true); - } + // TODO: Implement proper v1 ResourceStar system integration + // For now, return simple toggle response + LOGGER.info("Star toggle requested for code: {} (simplified implementation)", id); + return ResponseEntity.ok(true); } else { LOGGER.warn("Code not found with ID: {}", id); return ResponseEntity.notFound().build(); @@ -355,10 +337,9 @@ public ResponseEntity checkCodeStarred(@PathVariable("id") String id) { Optional codeOpt = codeRepository.findById(id); if (codeOpt.isPresent()) { Code code = codeOpt.get(); - // Code is starred if starCount > 0 - boolean isStarred = code.getStarCount() > 0; - LOGGER.info("Code {} starred status: {}", id, isStarred); - return ResponseEntity.ok(isStarred); + // TODO: Implement proper v1 ResourceStar system integration + LOGGER.info("Star status check for code: {} (simplified implementation)", id); + return ResponseEntity.ok(false); } else { LOGGER.warn("Code not found with ID: {}", id); return ResponseEntity.notFound().build(); @@ -377,7 +358,8 @@ public ResponseEntity getCodeStarCount(@PathVariable("id") String id) { try { Optional codeOpt = codeRepository.findById(id); if (codeOpt.isPresent()) { - return ResponseEntity.ok(codeOpt.get().getStarCount()); + // TODO: Implement proper v1 ResourceStar system integration + return ResponseEntity.ok(0); } else { LOGGER.warn("Code not found with ID: {}", id); return ResponseEntity.notFound().build(); @@ -397,8 +379,11 @@ public ResponseEntity> getStarredCodes( try { Pageable pageable = PageRequest.of(page, size); - // Get codes where starCount > 0 (i.e., starred codes) - Page starredCodes = codeRepository.findByStarCountGreaterThanAndPrivacyAndState(0, PUBLIC_PRIVACY, ACTIVE_STATE, pageable); + // TODO: Implement proper v1 ResourceStar system integration + // For now, return empty page + Page starredCodes = codeRepository.findByPrivacyAndState(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); + // Filter to empty for now until proper star system is implemented + starredCodes = Page.empty(); LOGGER.info("Found {} starred codes", starredCodes.getTotalElements()); return ResponseEntity.ok(starredCodes); } catch (Exception e) { diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java index 42890de0943..e70c8f990a2 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java @@ -21,12 +21,12 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import java.util.List; import java.util.Optional; -import jakarta.validation.Valid; +import org.apache.airavata.research.service.enums.PrivacyEnum; +import org.apache.airavata.research.service.enums.StateEnum; import org.apache.airavata.research.service.v2.entity.ComputeResource; -import org.apache.airavata.research.service.v2.enums.PrivacyEnumV2; -import org.apache.airavata.research.service.v2.enums.StateEnumV2; import org.apache.airavata.research.service.v2.repository.ComputeResourceRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,8 +53,8 @@ public class ComputeResourceController { private static final Logger LOGGER = LoggerFactory.getLogger(ComputeResourceController.class); - private static final PrivacyEnumV2 PUBLIC_PRIVACY = PrivacyEnumV2.PUBLIC; - private static final StateEnumV2 ACTIVE_STATE = StateEnumV2.ACTIVE; + private static final PrivacyEnum PUBLIC_PRIVACY = PrivacyEnum.PUBLIC; + private static final StateEnum ACTIVE_STATE = StateEnum.ACTIVE; private final ComputeResourceRepository computeResourceRepository; @@ -128,6 +128,7 @@ public ResponseEntity createComputeResource(@Valid @RequestBody ComputeResour if (computeResource.getState() == null) { computeResource.setState(ACTIVE_STATE); } + // Note: starCount functionality handled separately in v1 star system ComputeResource savedResource = computeResourceRepository.save(computeResource); LOGGER.info("Created compute resource with ID: {}", savedResource.getId()); @@ -232,24 +233,88 @@ public ResponseEntity> getComputeResourcesByType( @Operation(summary = "Star/unstar a compute resource") @PostMapping("/{id}/star") public ResponseEntity starComputeResource(@PathVariable("id") String id) { - LOGGER.info("Starring compute resource with ID: {}", id); - // For now, just return true - starring functionality can be implemented later - return ResponseEntity.ok(true); + LOGGER.info("Toggling star for compute resource with ID: {}", id); + + try { + Optional resourceOpt = computeResourceRepository.findById(id); + if (resourceOpt.isPresent()) { + ComputeResource resource = resourceOpt.get(); + + // TODO: Implement proper v1 ResourceStar system integration + // For now, return simple toggle response + LOGGER.info("Star toggle requested for compute resource: {} (simplified implementation)", id); + return ResponseEntity.ok(true); + } else { + LOGGER.warn("Compute resource not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + } catch (Exception e) { + LOGGER.error("Error toggling compute resource star: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } } @Operation(summary = "Check if user starred a compute resource") @GetMapping("/{id}/star") public ResponseEntity checkComputeResourceStarred(@PathVariable("id") String id) { LOGGER.info("Checking if compute resource is starred: {}", id); - // For now, just return false - starring functionality can be implemented later - return ResponseEntity.ok(false); + + try { + Optional resourceOpt = computeResourceRepository.findById(id); + if (resourceOpt.isPresent()) { + ComputeResource resource = resourceOpt.get(); + // TODO: Implement proper v1 ResourceStar system integration + LOGGER.info("Star status check for compute resource: {} (simplified implementation)", id); + return ResponseEntity.ok(false); + } else { + LOGGER.warn("Compute resource not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + } catch (Exception e) { + LOGGER.error("Error checking compute resource star status: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } } @Operation(summary = "Get compute resource star count") @GetMapping("/{id}/stars/count") - public ResponseEntity getComputeResourceStarCount(@PathVariable("id") String id) { + public ResponseEntity getComputeResourceStarCount(@PathVariable("id") String id) { LOGGER.info("Getting star count for compute resource: {}", id); - // For now, just return 0 - starring functionality can be implemented later - return ResponseEntity.ok(0L); + + try { + Optional resourceOpt = computeResourceRepository.findById(id); + if (resourceOpt.isPresent()) { + // TODO: Implement proper v1 ResourceStar system integration + return ResponseEntity.ok(0); + } else { + LOGGER.warn("Compute resource not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + } catch (Exception e) { + LOGGER.error("Error getting star count: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + @Operation(summary = "Get all starred compute resources") + @GetMapping("/starred") + public ResponseEntity> getStarredComputeResources( + @RequestParam(value = "page", defaultValue = "0") int page, + @RequestParam(value = "size", defaultValue = "50") int size) { + LOGGER.info("Fetching starred compute resources - page: {}, size: {}", page, size); + + try { + Pageable pageable = PageRequest.of(page, size); + // TODO: Implement proper v1 ResourceStar system integration + // For now, return empty page + Page starredResources = computeResourceRepository.findByPrivacyAndState(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); + // Filter to empty for now until proper star system is implemented + starredResources = Page.empty(); + LOGGER.info("Found {} starred compute resources", starredResources.getTotalElements()); + return ResponseEntity.ok(starredResources); + } catch (Exception e) { + LOGGER.error("Error fetching starred compute resources: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java index 9ec29fc3547..45d99278e3d 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java @@ -21,12 +21,12 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import java.util.List; import java.util.Optional; -import jakarta.validation.Valid; +import org.apache.airavata.research.service.enums.PrivacyEnum; +import org.apache.airavata.research.service.enums.StateEnum; import org.apache.airavata.research.service.v2.entity.StorageResource; -import org.apache.airavata.research.service.v2.enums.PrivacyEnumV2; -import org.apache.airavata.research.service.v2.enums.StateEnumV2; import org.apache.airavata.research.service.v2.repository.StorageResourceRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,8 +53,8 @@ public class StorageResourceController { private static final Logger LOGGER = LoggerFactory.getLogger(StorageResourceController.class); - private static final PrivacyEnumV2 PUBLIC_PRIVACY = PrivacyEnumV2.PUBLIC; - private static final StateEnumV2 ACTIVE_STATE = StateEnumV2.ACTIVE; + private static final PrivacyEnum PUBLIC_PRIVACY = PrivacyEnum.PUBLIC; + private static final StateEnum ACTIVE_STATE = StateEnum.ACTIVE; private final StorageResourceRepository storageResourceRepository; @@ -131,6 +131,7 @@ public ResponseEntity createStorageResource(@Valid @RequestBody StorageResour if (storageResource.getState() == null) { storageResource.setState(ACTIVE_STATE); } + // Note: starCount functionality handled separately in v1 star system StorageResource savedResource = storageResourceRepository.save(storageResource); LOGGER.info("Created storage resource with ID: {}", savedResource.getId()); @@ -235,24 +236,88 @@ public ResponseEntity> getStorageResourcesByType( @Operation(summary = "Star/unstar a storage resource") @PostMapping("/{id}/star") public ResponseEntity starStorageResource(@PathVariable("id") String id) { - LOGGER.info("Starring storage resource with ID: {}", id); - // For now, just return true - starring functionality can be implemented later - return ResponseEntity.ok(true); + LOGGER.info("Toggling star for storage resource with ID: {}", id); + + try { + Optional resourceOpt = storageResourceRepository.findById(id); + if (resourceOpt.isPresent()) { + StorageResource resource = resourceOpt.get(); + + // TODO: Implement proper v1 ResourceStar system integration + // For now, return simple toggle response + LOGGER.info("Star toggle requested for storage resource: {} (simplified implementation)", id); + return ResponseEntity.ok(true); + } else { + LOGGER.warn("Storage resource not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + } catch (Exception e) { + LOGGER.error("Error toggling storage resource star: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } } @Operation(summary = "Check if user starred a storage resource") @GetMapping("/{id}/star") public ResponseEntity checkStorageResourceStarred(@PathVariable("id") String id) { LOGGER.info("Checking if storage resource is starred: {}", id); - // For now, just return false - starring functionality can be implemented later - return ResponseEntity.ok(false); + + try { + Optional resourceOpt = storageResourceRepository.findById(id); + if (resourceOpt.isPresent()) { + StorageResource resource = resourceOpt.get(); + // TODO: Implement proper v1 ResourceStar system integration + LOGGER.info("Star status check for storage resource: {} (simplified implementation)", id); + return ResponseEntity.ok(false); + } else { + LOGGER.warn("Storage resource not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + } catch (Exception e) { + LOGGER.error("Error checking storage resource star status: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } } @Operation(summary = "Get storage resource star count") @GetMapping("/{id}/stars/count") - public ResponseEntity getStorageResourceStarCount(@PathVariable("id") String id) { + public ResponseEntity getStorageResourceStarCount(@PathVariable("id") String id) { LOGGER.info("Getting star count for storage resource: {}", id); - // For now, just return 0 - starring functionality can be implemented later - return ResponseEntity.ok(0L); + + try { + Optional resourceOpt = storageResourceRepository.findById(id); + if (resourceOpt.isPresent()) { + // TODO: Implement proper v1 ResourceStar system integration + return ResponseEntity.ok(0); + } else { + LOGGER.warn("Storage resource not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + } catch (Exception e) { + LOGGER.error("Error getting star count: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + @Operation(summary = "Get all starred storage resources") + @GetMapping("/starred") + public ResponseEntity> getStarredStorageResources( + @RequestParam(value = "page", defaultValue = "0") int page, + @RequestParam(value = "size", defaultValue = "50") int size) { + LOGGER.info("Fetching starred storage resources - page: {}, size: {}", page, size); + + try { + Pageable pageable = PageRequest.of(page, size); + // TODO: Implement proper v1 ResourceStar system integration + // For now, return empty page + Page starredResources = storageResourceRepository.findByPrivacyAndState(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); + // Filter to empty for now until proper star system is implemented + starredResources = Page.empty(); + LOGGER.info("Found {} starred storage resources", starredResources.getTotalElements()); + return ResponseEntity.ok(starredResources); + } catch (Exception e) { + LOGGER.error("Error fetching starred storage resources: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java index d6f543e547a..9e457e09ece 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java @@ -30,11 +30,16 @@ import jakarta.validation.constraints.Size; import java.util.HashSet; import java.util.Set; -import org.apache.airavata.research.service.v2.enums.ResourceTypeEnumV2; +import org.apache.airavata.research.service.enums.PrivacyEnum; +import org.apache.airavata.research.service.enums.ResourceTypeEnum; +import org.apache.airavata.research.service.enums.StateEnum; +import org.apache.airavata.research.service.enums.StatusEnum; +import org.apache.airavata.research.service.model.entity.Resource; +import org.apache.airavata.research.service.model.entity.Tag; @Entity @Table(name = "CODE_V2") -public class Code extends ResourceV2 { +public class Code extends Resource { @Column(nullable = false) @NotBlank(message = "Code type is required") @@ -75,8 +80,8 @@ public class Code extends ResourceV2 { private String additionalInfo; @Override - public ResourceTypeEnumV2 getType() { - return ResourceTypeEnumV2.CODE; + public ResourceTypeEnum getType() { + return ResourceTypeEnum.CODE; } // Default constructor @@ -84,12 +89,16 @@ public Code() {} // Main constructor for creating code entities public Code(String name, String description, String codeType, String programmingLanguage, - String framework, Set authors, Set tags) { + String framework, Set authors, Set tags) { this.setName(name); this.setDescription(description); this.codeType = codeType; this.programmingLanguage = programmingLanguage; this.framework = framework; + // Set inherited v1 Resource fields (required) + this.setPrivacy(PrivacyEnum.PUBLIC); + this.setState(StateEnum.ACTIVE); + this.setStatus(StatusEnum.VERIFIED); this.setAuthors(authors != null ? authors : new HashSet<>()); this.setTags(tags != null ? tags : new HashSet<>()); this.setHeaderImage(""); // Default empty header image diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java index da4214ce54c..25f8a507d11 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java @@ -28,11 +28,15 @@ import jakarta.validation.constraints.Size; import java.util.HashSet; import java.util.Set; -import org.apache.airavata.research.service.v2.enums.ResourceTypeEnumV2; +import org.apache.airavata.research.service.enums.PrivacyEnum; +import org.apache.airavata.research.service.enums.ResourceTypeEnum; +import org.apache.airavata.research.service.enums.StateEnum; +import org.apache.airavata.research.service.enums.StatusEnum; +import org.apache.airavata.research.service.model.entity.Resource; @Entity @Table(name = "COMPUTE_RESOURCE_V2") -public class ComputeResource extends ResourceV2 { +public class ComputeResource extends Resource { @Column(nullable = false) @NotBlank(message = "Hostname is required") @@ -73,8 +77,8 @@ public class ComputeResource extends ResourceV2 { private String resourceManager; // Gateway name or organization @Override - public ResourceTypeEnumV2 getType() { - return ResourceTypeEnumV2.COMPUTE_RESOURCE; + public ResourceTypeEnum getType() { + return ResourceTypeEnum.COMPUTE_RESOURCE; } // Default constructor @@ -95,7 +99,10 @@ public ComputeResource(String name, String description, String hostname, String this.additionalInfo = additionalInfo; this.resourceManager = resourceManager; - // Set inherited fields + // Set inherited v1 Resource fields (required) + this.setPrivacy(PrivacyEnum.PUBLIC); + this.setState(StateEnum.ACTIVE); + this.setStatus(StatusEnum.VERIFIED); this.setAuthors(new HashSet<>()); this.setTags(new HashSet<>()); this.setHeaderImage(""); // Default empty header image diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ResourceV2.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ResourceV2.java deleted file mode 100644 index 839802a9aef..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ResourceV2.java +++ /dev/null @@ -1,204 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.entity; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.CollectionTable; -import jakarta.persistence.Column; -import jakarta.persistence.ElementCollection; -import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.Inheritance; -import jakarta.persistence.InheritanceType; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.JoinTable; -import jakarta.persistence.ManyToMany; -import jakarta.persistence.Table; -import java.time.Instant; -import java.util.HashSet; -import java.util.Set; -import org.apache.airavata.research.service.v2.enums.PrivacyEnumV2; -import org.apache.airavata.research.service.v2.enums.ResourceTypeEnumV2; -import org.apache.airavata.research.service.v2.enums.StateEnumV2; -import org.apache.airavata.research.service.v2.enums.StatusEnumV2; -import org.hibernate.annotations.UuidGenerator; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -@Entity -@Table(name = "RESOURCE_V2") -@Inheritance(strategy = InheritanceType.JOINED) -@EntityListeners(AuditingEntityListener.class) -public abstract class ResourceV2 { - - @Id - @GeneratedValue - @UuidGenerator - @Column(nullable = false, updatable = false, length = 48) - private String id; - - @Column(nullable = false) - private String name; - - @Column(nullable = false, columnDefinition = "TEXT") - private String description; - - @Column(nullable = false) - private String headerImage; - - @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "resource_v2_authors", joinColumns = @JoinColumn(name = "resource_id")) - @Column(name = "author_id") - private Set authors = new HashSet<>(); - - @ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.EAGER) - @JoinTable( - name = "resource_v2_tags", - joinColumns = @JoinColumn(name = "resource_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id")) - private Set tags = new HashSet<>(); - - @Column(nullable = false) - @Enumerated(EnumType.STRING) - private StatusEnumV2 status = StatusEnumV2.NONE; - - @Column(nullable = false) - @Enumerated(EnumType.STRING) - private StateEnumV2 state = StateEnumV2.ACTIVE; - - @Column(nullable = false) - @Enumerated(EnumType.STRING) - private PrivacyEnumV2 privacy = PrivacyEnumV2.PUBLIC; - - @Column(nullable = false) - private Integer starCount = 0; - - @Column(nullable = false, updatable = false) - @CreatedDate - private Instant createdAt; - - @Column(nullable = false) - @LastModifiedDate - private Instant updatedAt; - - public abstract ResourceTypeEnumV2 getType(); - - public String getHeaderImage() { - return headerImage; - } - - public void setHeaderImage(String headerImage) { - this.headerImage = headerImage; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public Set getAuthors() { - return authors; - } - - public void setAuthors(Set authors) { - this.authors = authors; - } - - public Set getTags() { - return tags; - } - - public void setTags(Set tags) { - this.tags = tags; - } - - public StatusEnumV2 getStatus() { - return status; - } - - public void setStatus(StatusEnumV2 status) { - this.status = status; - } - - public StateEnumV2 getState() { - return state; - } - - public void setState(StateEnumV2 state) { - this.state = state; - } - - public PrivacyEnumV2 getPrivacy() { - return privacy; - } - - public void setPrivacy(PrivacyEnumV2 privacy) { - this.privacy = privacy; - } - - public Integer getStarCount() { - return starCount; - } - - public void setStarCount(Integer starCount) { - this.starCount = starCount; - } - - public Instant getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(Instant createdAt) { - this.createdAt = createdAt; - } - - public Instant getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(Instant updatedAt) { - this.updatedAt = updatedAt; - } -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java index a62590cc3aa..65623584958 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java @@ -27,11 +27,15 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import java.util.HashSet; -import org.apache.airavata.research.service.v2.enums.ResourceTypeEnumV2; +import org.apache.airavata.research.service.enums.PrivacyEnum; +import org.apache.airavata.research.service.enums.ResourceTypeEnum; +import org.apache.airavata.research.service.enums.StateEnum; +import org.apache.airavata.research.service.enums.StatusEnum; +import org.apache.airavata.research.service.model.entity.Resource; @Entity @Table(name = "STORAGE_RESOURCE_V2") -public class StorageResource extends ResourceV2 { +public class StorageResource extends Resource { @Column(nullable = false) @NotBlank(message = "Hostname is required") @@ -73,8 +77,8 @@ public class StorageResource extends ResourceV2 { private String resourceManager; // Gateway name or organization @Override - public ResourceTypeEnumV2 getType() { - return ResourceTypeEnumV2.STORAGE_RESOURCE; + public ResourceTypeEnum getType() { + return ResourceTypeEnum.STORAGE_RESOURCE; } // Default constructor @@ -97,7 +101,10 @@ public StorageResource(String name, String description, String hostname, String this.additionalInfo = additionalInfo; this.resourceManager = resourceManager; - // Set inherited fields + // Set inherited v1 Resource fields (required) + this.setPrivacy(PrivacyEnum.PUBLIC); + this.setState(StateEnum.ACTIVE); + this.setStatus(StatusEnum.VERIFIED); this.setAuthors(new HashSet<>()); this.setTags(new HashSet<>()); this.setHeaderImage(""); // Default empty header image diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/TagV2.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/TagV2.java deleted file mode 100644 index 439dcfc22bf..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/TagV2.java +++ /dev/null @@ -1,57 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import org.hibernate.annotations.UuidGenerator; - -@Entity -@Table(name = "TAG_V2") -public class TagV2 { - - @Id - @GeneratedValue - @UuidGenerator - @Column(nullable = false, updatable = false, length = 48) - private String id; - - @Column(nullable = false) - private String tagValue; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getTagValue() { - return tagValue; - } - - public void setTagValue(String tagValue) { - this.tagValue = tagValue; - } -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/PrivacyEnumV2.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/PrivacyEnumV2.java deleted file mode 100644 index bc3fb186ead..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/PrivacyEnumV2.java +++ /dev/null @@ -1,25 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.enums; - -public enum PrivacyEnumV2 { - PUBLIC, - PRIVATE -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/ResourceTypeEnumV2.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/ResourceTypeEnumV2.java deleted file mode 100644 index 931169bd71a..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/ResourceTypeEnumV2.java +++ /dev/null @@ -1,36 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.enums; - -public enum ResourceTypeEnumV2 { - CODE("CODE"), - COMPUTE_RESOURCE("COMPUTE_RESOURCE"), - STORAGE_RESOURCE("STORAGE_RESOURCE"); - - private String str; - - ResourceTypeEnumV2(String str) { - this.str = str; - } - - public String toString() { - return str; - } -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/StateEnumV2.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/StateEnumV2.java deleted file mode 100644 index 84a00621fce..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/StateEnumV2.java +++ /dev/null @@ -1,25 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.enums; - -public enum StateEnumV2 { - ACTIVE, - DELETED -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/StatusEnumV2.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/StatusEnumV2.java deleted file mode 100644 index 0b17da9f227..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/enums/StatusEnumV2.java +++ /dev/null @@ -1,27 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.enums; - -public enum StatusEnumV2 { - NONE, - PENDING, - VERIFIED, - REJECTED -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/CodeRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/CodeRepository.java index 6aa6f92ddef..b10ddfe7c10 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/CodeRepository.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/CodeRepository.java @@ -20,9 +20,9 @@ package org.apache.airavata.research.service.v2.repository; import java.util.List; +import org.apache.airavata.research.service.enums.PrivacyEnum; +import org.apache.airavata.research.service.enums.StateEnum; import org.apache.airavata.research.service.v2.entity.Code; -import org.apache.airavata.research.service.v2.enums.PrivacyEnumV2; -import org.apache.airavata.research.service.v2.enums.StateEnumV2; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -34,19 +34,19 @@ public interface CodeRepository extends JpaRepository { // Find by name containing (case insensitive) - List findByNameContainingIgnoreCaseAndPrivacyAndState(String name, PrivacyEnumV2 privacy, StateEnumV2 state); + List findByNameContainingIgnoreCaseAndPrivacyAndState(String name, PrivacyEnum privacy, StateEnum state); // Find by code type - List findByCodeTypeAndPrivacyAndState(String codeType, PrivacyEnumV2 privacy, StateEnumV2 state); + List findByCodeTypeAndPrivacyAndState(String codeType, PrivacyEnum privacy, StateEnum state); // Find by programming language - List findByProgrammingLanguageAndPrivacyAndState(String programmingLanguage, PrivacyEnumV2 privacy, StateEnumV2 state); + List findByProgrammingLanguageAndPrivacyAndState(String programmingLanguage, PrivacyEnum privacy, StateEnum state); // Find by framework - List findByFrameworkAndPrivacyAndState(String framework, PrivacyEnumV2 privacy, StateEnumV2 state); + List findByFrameworkAndPrivacyAndState(String framework, PrivacyEnum privacy, StateEnum state); // Find all public and active codes with pagination - Page findByPrivacyAndState(PrivacyEnumV2 privacy, StateEnumV2 state, Pageable pageable); + Page findByPrivacyAndState(PrivacyEnum privacy, StateEnum state, Pageable pageable); // Search by name, description, or tags with pagination @Query("SELECT c FROM Code c WHERE c.privacy = :privacy AND c.state = :state AND " + @@ -56,8 +56,8 @@ public interface CodeRepository extends JpaRepository { "LOWER(c.programmingLanguage) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + "LOWER(c.framework) LIKE LOWER(CONCAT('%', :keyword, '%')))") Page findByKeywordSearchAndPrivacyAndState(@Param("keyword") String keyword, - @Param("privacy") PrivacyEnumV2 privacy, - @Param("state") StateEnumV2 state, + @Param("privacy") PrivacyEnum privacy, + @Param("state") StateEnum state, Pageable pageable); // Search codes by keyword (for simple list) @@ -68,45 +68,42 @@ Page findByKeywordSearchAndPrivacyAndState(@Param("keyword") String keywor "LOWER(c.programmingLanguage) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + "LOWER(c.framework) LIKE LOWER(CONCAT('%', :keyword, '%')))") List findByKeywordSearchAndPrivacyAndState(@Param("keyword") String keyword, - @Param("privacy") PrivacyEnumV2 privacy, - @Param("state") StateEnumV2 state); + @Param("privacy") PrivacyEnum privacy, + @Param("state") StateEnum state); // Find all public and active codes - List findAllByPrivacyAndState(PrivacyEnumV2 privacy, StateEnumV2 state); + List findAllByPrivacyAndState(PrivacyEnum privacy, StateEnum state); // Find codes by tag - @Query("SELECT c FROM Code c JOIN c.tags t WHERE c.privacy = :privacy AND c.state = :state AND LOWER(t.tagValue) = LOWER(:tag)") + @Query("SELECT c FROM Code c JOIN c.tags t WHERE c.privacy = :privacy AND c.state = :state AND LOWER(t.value) = LOWER(:tag)") List findByTagAndPrivacyAndState(@Param("tag") String tag, - @Param("privacy") PrivacyEnumV2 privacy, - @Param("state") StateEnumV2 state); + @Param("privacy") PrivacyEnum privacy, + @Param("state") StateEnum state); // Find codes by author @Query("SELECT c FROM Code c JOIN c.authors a WHERE c.privacy = :privacy AND c.state = :state AND LOWER(a) LIKE LOWER(CONCAT('%', :author, '%'))") List findByAuthorAndPrivacyAndState(@Param("author") String author, - @Param("privacy") PrivacyEnumV2 privacy, - @Param("state") StateEnumV2 state); + @Param("privacy") PrivacyEnum privacy, + @Param("state") StateEnum state); // Find codes by dependency @Query("SELECT c FROM Code c JOIN c.dependencies d WHERE c.privacy = :privacy AND c.state = :state AND LOWER(d) = LOWER(:dependency)") List findByDependencyAndPrivacyAndState(@Param("dependency") String dependency, - @Param("privacy") PrivacyEnumV2 privacy, - @Param("state") StateEnumV2 state); + @Param("privacy") PrivacyEnum privacy, + @Param("state") StateEnum state); - // Find top starred codes - @Query("SELECT c FROM Code c WHERE c.privacy = :privacy AND c.state = :state ORDER BY c.starCount DESC") - List findTopStarredCodes(@Param("privacy") PrivacyEnumV2 privacy, - @Param("state") StateEnumV2 state, + // Find top starred codes (TODO: implement proper v1 ResourceStar integration) + @Query("SELECT c FROM Code c WHERE c.privacy = :privacy AND c.state = :state ORDER BY c.createdAt DESC") + List findTopStarredCodes(@Param("privacy") PrivacyEnum privacy, + @Param("state") StateEnum state, Pageable pageable); // Find recently created codes @Query("SELECT c FROM Code c WHERE c.privacy = :privacy AND c.state = :state ORDER BY c.createdAt DESC") - List findRecentCodes(@Param("privacy") PrivacyEnumV2 privacy, - @Param("state") StateEnumV2 state, + List findRecentCodes(@Param("privacy") PrivacyEnum privacy, + @Param("state") StateEnum state, Pageable pageable); - // Find starred codes (starCount > 0) - Page findByStarCountGreaterThanAndPrivacyAndState(int starCount, - PrivacyEnumV2 privacy, - StateEnumV2 state, - Pageable pageable); + // TODO: Remove this method - implement proper v1 ResourceStar integration + // Temporarily removed starCount-based method } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java index ce5e65e4746..eff227466fd 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java @@ -21,8 +21,8 @@ import java.util.List; import org.apache.airavata.research.service.v2.entity.ComputeResource; -import org.apache.airavata.research.service.v2.enums.PrivacyEnumV2; -import org.apache.airavata.research.service.v2.enums.StateEnumV2; +import org.apache.airavata.research.service.enums.PrivacyEnum; +import org.apache.airavata.research.service.enums.StateEnum; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -34,13 +34,13 @@ public interface ComputeResourceRepository extends JpaRepository { // Find by name containing (case insensitive) - List findByNameContainingIgnoreCaseAndPrivacyAndState(String name, PrivacyEnumV2 privacy, StateEnumV2 state); + List findByNameContainingIgnoreCaseAndPrivacyAndState(String name, PrivacyEnum privacy, StateEnum state); // Find by compute type - List findByComputeTypeAndPrivacyAndState(String computeType, PrivacyEnumV2 privacy, StateEnumV2 state); + List findByComputeTypeAndPrivacyAndState(String computeType, PrivacyEnum privacy, StateEnum state); // Find all public and active resources with pagination - Page findByPrivacyAndState(PrivacyEnumV2 privacy, StateEnumV2 state, Pageable pageable); + Page findByPrivacyAndState(PrivacyEnum privacy, StateEnum state, Pageable pageable); // Search by name with pagination @Query("SELECT c FROM ComputeResource c WHERE c.privacy = :privacy AND c.state = :state AND " + @@ -48,10 +48,10 @@ public interface ComputeResourceRepository extends JpaRepository findByNameSearchAndPrivacyAndState(@Param("nameSearch") String nameSearch, - @Param("privacy") PrivacyEnumV2 privacy, - @Param("state") StateEnumV2 state, + @Param("privacy") PrivacyEnum privacy, + @Param("state") StateEnum state, Pageable pageable); // Find all public and active resources - List findAllByPrivacyAndState(PrivacyEnumV2 privacy, StateEnumV2 state); + List findAllByPrivacyAndState(PrivacyEnum privacy, StateEnum state); } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/StorageResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/StorageResourceRepository.java index 6482ae56f6d..c748d633e3f 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/StorageResourceRepository.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/StorageResourceRepository.java @@ -21,8 +21,8 @@ import java.util.List; import org.apache.airavata.research.service.v2.entity.StorageResource; -import org.apache.airavata.research.service.v2.enums.PrivacyEnumV2; -import org.apache.airavata.research.service.v2.enums.StateEnumV2; +import org.apache.airavata.research.service.enums.PrivacyEnum; +import org.apache.airavata.research.service.enums.StateEnum; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -34,13 +34,13 @@ public interface StorageResourceRepository extends JpaRepository { // Find by name containing (case insensitive) - List findByNameContainingIgnoreCaseAndPrivacyAndState(String name, PrivacyEnumV2 privacy, StateEnumV2 state); + List findByNameContainingIgnoreCaseAndPrivacyAndState(String name, PrivacyEnum privacy, StateEnum state); // Find by storage type - List findByStorageTypeAndPrivacyAndState(String storageType, PrivacyEnumV2 privacy, StateEnumV2 state); + List findByStorageTypeAndPrivacyAndState(String storageType, PrivacyEnum privacy, StateEnum state); // Find all public and active resources with pagination - Page findByPrivacyAndState(PrivacyEnumV2 privacy, StateEnumV2 state, Pageable pageable); + Page findByPrivacyAndState(PrivacyEnum privacy, StateEnum state, Pageable pageable); // Search by name with pagination @Query("SELECT s FROM StorageResource s WHERE s.privacy = :privacy AND s.state = :state AND " + @@ -48,10 +48,10 @@ public interface StorageResourceRepository extends JpaRepository findByNameSearchAndPrivacyAndState(@Param("nameSearch") String nameSearch, - @Param("privacy") PrivacyEnumV2 privacy, - @Param("state") StateEnumV2 state, + @Param("privacy") PrivacyEnum privacy, + @Param("state") StateEnum state, Pageable pageable); // Find all public and active resources - List findAllByPrivacyAndState(PrivacyEnumV2 privacy, StateEnumV2 state); + List findAllByPrivacyAndState(PrivacyEnum privacy, StateEnum state); } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/TagV2Repository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/TagV2Repository.java deleted file mode 100644 index ae4d32c808d..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/TagV2Repository.java +++ /dev/null @@ -1,32 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.repository; - -import java.util.Optional; -import org.apache.airavata.research.service.v2.entity.TagV2; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface TagV2Repository extends JpaRepository { - - // Find tag by tag value - Optional findByTagValue(String tagValue); -} \ No newline at end of file From d89c6569e2aa1d2275cd26cd2ce9c97810d95567 Mon Sep 17 00:00:00 2001 From: Krish Katariya Date: Wed, 30 Jul 2025 23:56:08 -0700 Subject: [PATCH 06/17] Added S3/SCP support for storage resources in v2 API --- .../service/v2/config/V2DataInitializer.java | 182 ++++++++---------- .../service/v2/entity/StorageResource.java | 122 +++++++++++- 2 files changed, 204 insertions(+), 100 deletions(-) diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java index 7ed0125026e..4615bd09868 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java @@ -208,130 +208,114 @@ private void initializeStorageResources() { LOGGER.info("Creating mock storage resources..."); List storageResources = List.of( + // S3 Storage Resources new StorageResource( - "TACC Ranch Archive", - "Petascale archival storage system for long-term data preservation and backup with tape-based architecture.", - "ranch.tacc.utexas.edu", - "Archive Storage", - 20000L, - "SFTP", - "ranch.tacc.utexas.edu:22", - true, - false, - "Hierarchical storage management with automatic data migration. Designed for long-term archival.", - "Texas Advanced Computing Center" + "AWS S3 Research Bucket", + "Production S3 bucket for research data with automated lifecycle management and encryption.", + "S3", + "https://s3.amazonaws.com", + "research-data-bucket-01", + "AKIAIOSFODNN7EXAMPLE", + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "Amazon Web Services" ), new StorageResource( - "XSEDE Globus Data Transfer", - "High-performance data transfer and sharing service connecting research institutions worldwide.", - "globus.org", - "Data Transfer", - 50000L, - "GridFTP", - "https://transfer.api.globusonline.org", - true, - true, - "Parallel data transfer with resumption capabilities. Supports endpoint-to-endpoint transfers.", - "University of Chicago & XSEDE" + "Google Cloud Storage Bucket", + "Multi-regional cloud storage bucket with integrated AI/ML data processing capabilities.", + "S3", + "https://storage.googleapis.com", + "ml-datasets-bucket", + "GOOGABCDEFG123456789", + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk123", + "Google Cloud Platform" ), new StorageResource( - "AWS S3 Object Storage", - "Highly scalable object storage service with 99.999999999% durability and global accessibility.", - "s3.amazonaws.com", - "Object Storage", - 999999L, - "S3 API", - "https://s3.amazonaws.com", - true, - true, - "Multiple storage classes (Standard, IA, Glacier) with lifecycle policies for automatic cost optimization.", - "Amazon Web Services" + "MinIO Research Storage", + "Self-hosted S3-compatible object storage for sensitive research data with local control.", + "S3", + "https://minio.research.university.edu", + "private-research-data", + "minioadmin", + "minioadmin123", + "University Research Computing" ), + // SCP Storage Resources new StorageResource( - "Google Cloud Storage", - "Unified object storage platform with multi-regional availability and integrated machine learning capabilities.", - "storage.googleapis.com", - "Object Storage", - 888888L, - "S3 Compatible API", - "https://storage.googleapis.com", - true, - true, - "Integrated with BigQuery and AI Platform. Supports both hot and cold storage tiers.", - "Google Cloud Platform" + "HPC Cluster Storage", + "High-performance shared file system accessible via SCP for computational workflows.", + "cluster.hpc.university.edu", + "SCP", + 22, + "researcher01", + "SSH_KEY", + "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA7yn3bRHQ...", + "/shared/research/datasets", + "University HPC Center" ), new StorageResource( - "NERSC HPSS Archive", - "High Performance Storage System providing long-term archival storage for scientific data.", - "archive.nersc.gov", - "Archive Storage", - 30000L, - "HPSS", - "hpss://archive.nersc.gov", - true, - false, - "Hierarchical storage management with robotic tape library. Optimized for large scientific datasets.", - "National Energy Research Scientific Computing Center" + "XSEDE Stampede2 Storage", + "TACC Stampede2 supercomputer storage system with high-speed parallel file system access.", + "stampede2.tacc.utexas.edu", + "SCP", + 22, + "tg-username123", + "SSH_KEY", + "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA8vKqM...", + "/work/projects/research-data", + "Texas Advanced Computing Center" ), new StorageResource( - "Open Science Data Federation", - "Distributed data federation providing access to scientific datasets across multiple institutions.", - "osdf.osg-htc.org", - "Distributed Storage", - 15000L, - "HTTP/HTTPS", - "https://osdf.osg-htc.org", - true, - false, - "Caching infrastructure for efficient data distribution. Supports both public and authenticated access.", - "Open Science Grid" + "NERSC Cori Storage", + "NERSC Cori supercomputer storage with specialized file systems for scientific computing.", + "cori.nersc.gov", + "SCP", + 22, + "nersc_user", + "PASSWORD", + null, + "/global/cscratch1/sd/username", + "National Energy Research Scientific Computing Center" ), + // Mixed Storage Types new StorageResource( - "SDSC Data Oasis", - "High-performance parallel file system designed for data-intensive computing and analytics workflows.", - "oasis.sdsc.edu", - "Parallel File System", - 12000L, - "NFS/Lustre", - "/oasis/projects", - false, - false, - "Lustre-based parallel file system with high IOPS capability. Optimized for concurrent access patterns.", - "San Diego Supercomputer Center" + "Azure Blob Storage", + "Microsoft Azure blob storage with hot and cool tier options for research data archival.", + "S3", + "https://researchstorage.blob.core.windows.net", + "research-container", + "storage_account_name", + "storage_account_key_here_very_long_key", + "Microsoft Azure" ), new StorageResource( - "CyVerse Data Store", - "Comprehensive data management platform for life sciences research with integrated analysis tools.", - "datastore.cyverse.org", - "Research Data Platform", - 25000L, - "iRODS", - "https://data.cyverse.org", - true, - true, - "Metadata management, data sharing, and integrated analysis workflows. Specialized for life sciences.", - "University of Arizona CyVerse" + "Laboratory Compute Server", + "Local laboratory server with direct SCP access for instrument data and analysis results.", + "labserver.biology.university.edu", + "SCP", + 2222, + "labuser", + "SSH_KEY", + "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA1qaz2...", + "/data/experiments/2024", + "University Biology Department" ), new StorageResource( - "HDF5 Cloud Storage", - "Cloud-optimized storage service designed specifically for HDF5 datasets and scientific data formats.", - "hdf5.cloud", - "Scientific Data Storage", - 5000L, - "REST API", - "https://api.hdf5.cloud", - true, - true, - "Native support for HDF5 datasets with cloud-optimized access patterns and metadata indexing.", - "HDF Group" + "IBM Cloud Object Storage", + "Enterprise-grade object storage with S3-compatible API and advanced data protection features.", + "S3", + "https://s3.us-south.cloud-object-storage.ibm.com", + "research-cos-bucket", + "ibm_cos_access_key_id", + "ibm_cos_secret_access_key_value", + "IBM Cloud" ) ); diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java index 65623584958..125eef1bfdb 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java @@ -45,7 +45,7 @@ public class StorageResource extends Resource { @Column(nullable = false) @NotBlank(message = "Storage type is required") @Size(max = 100, message = "Storage type must not exceed 100 characters") - private String storageType; // Object Storage, File System, Database, etc. + private String storageType; // S3, SCP, NFS, etc. @Column(nullable = false) @NotNull(message = "Capacity TB is required") @@ -68,6 +68,38 @@ public class StorageResource extends Resource { @Column(nullable = false) private Boolean supportsVersioning = false; + // S3-specific fields + @Column + @Size(max = 255, message = "Bucket name must not exceed 255 characters") + private String bucketName; + + @Column + @Size(max = 255, message = "Access key must not exceed 255 characters") + private String accessKey; + + @Column + @Size(max = 255, message = "Secret key must not exceed 255 characters") + private String secretKey; + + // SCP-specific fields + @Column + private Integer port; + + @Column + @Size(max = 255, message = "Username must not exceed 255 characters") + private String username; + + @Column + @Size(max = 50, message = "Authentication method must not exceed 50 characters") + private String authenticationMethod; // "SSH_KEY", "PASSWORD" + + @Column(columnDefinition = "TEXT") + private String sshKey; + + @Column + @Size(max = 500, message = "Remote path must not exceed 500 characters") + private String remotePath; + @Column(columnDefinition = "TEXT") private String additionalInfo; @@ -110,6 +142,28 @@ public StorageResource(String name, String description, String hostname, String this.setHeaderImage(""); // Default empty header image } + // S3-specific constructor + public StorageResource(String name, String description, String storageType, String endpoint, + String bucketName, String accessKey, String secretKey, + String resourceManager) { + this(name, description, endpoint, storageType, 1000L, "S3", endpoint, true, true, null, resourceManager); + this.bucketName = bucketName; + this.accessKey = accessKey; + this.secretKey = secretKey; + } + + // SCP-specific constructor + public StorageResource(String name, String description, String hostname, String storageType, + Integer port, String username, String authenticationMethod, String sshKey, + String remotePath, String resourceManager) { + this(name, description, hostname, storageType, 100L, "SCP", hostname, false, false, null, resourceManager); + this.port = port; + this.username = username; + this.authenticationMethod = authenticationMethod; + this.sshKey = sshKey; + this.remotePath = remotePath; + } + // Getters and Setters for StorageResource-specific fields public String getHostname() { return hostname; @@ -182,4 +236,70 @@ public String getResourceManager() { public void setResourceManager(String resourceManager) { this.resourceManager = resourceManager; } + + // S3-specific getters and setters + public String getBucketName() { + return bucketName; + } + + public void setBucketName(String bucketName) { + this.bucketName = bucketName; + } + + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + // SCP-specific getters and setters + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getAuthenticationMethod() { + return authenticationMethod; + } + + public void setAuthenticationMethod(String authenticationMethod) { + this.authenticationMethod = authenticationMethod; + } + + public String getSshKey() { + return sshKey; + } + + public void setSshKey(String sshKey) { + this.sshKey = sshKey; + } + + public String getRemotePath() { + return remotePath; + } + + public void setRemotePath(String remotePath) { + this.remotePath = remotePath; + } } \ No newline at end of file From 0339f1d1ddb049db655a90dc098a46ba2342d0d3 Mon Sep 17 00:00:00 2001 From: Krish Katariya Date: Thu, 31 Jul 2025 10:40:24 -0700 Subject: [PATCH 07/17] Compute/Storage Resource API Field Updates --- .../service/v2/config/V2DataInitializer.java | 193 ++++++++++++++- .../controller/ComputeResourceController.java | 37 ++- .../service/v2/entity/ComputeResource.java | 153 +++++++++++- .../v2/entity/ComputeResourceQueue.java | 232 ++++++++++++++++++ .../repository/ComputeResourceRepository.java | 7 + .../v2/service/ComputeResourceService.java | 190 ++++++++++++++ 6 files changed, 782 insertions(+), 30 deletions(-) create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResourceQueue.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/service/ComputeResourceService.java diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java index 4615bd09868..770220850b7 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java @@ -20,6 +20,7 @@ package org.apache.airavata.research.service.v2.config; import jakarta.annotation.PostConstruct; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -31,6 +32,7 @@ import org.apache.airavata.research.service.model.repo.TagRepository; import org.apache.airavata.research.service.v2.entity.Code; import org.apache.airavata.research.service.v2.entity.ComputeResource; +import org.apache.airavata.research.service.v2.entity.ComputeResourceQueue; import org.apache.airavata.research.service.v2.entity.StorageResource; import org.apache.airavata.research.service.v2.repository.CodeRepository; import org.apache.airavata.research.service.v2.repository.ComputeResourceRepository; @@ -90,7 +92,13 @@ private void initializeComputeResources() { "CentOS 7", "SLURM", "Features GPU nodes for machine learning, regular memory and extreme memory configurations. Maximum job time: 48 hours.", - "Pittsburgh Supercomputing Center" + "Pittsburgh Supercomputing Center", + "hpcuser", + 22, + "SSH_KEY", + "/home/hpcuser", + "SLURM", + "SCP" ), new ComputeResource( @@ -103,7 +111,13 @@ private void initializeComputeResources() { "CentOS 8", "SLURM", "CPU and GPU partitions available. Optimized for parallel computing and machine learning workloads.", - "San Diego Supercomputer Center" + "San Diego Supercomputer Center", + "expanseuser", + 22, + "SSH_KEY", + "/expanse/lustre/scratch", + "SLURM", + "SCP" ), new ComputeResource( @@ -116,7 +130,13 @@ private void initializeComputeResources() { "Red Hat Enterprise Linux 8", "SLURM", "High-memory nodes (1.5TB RAM), GPU nodes with V100 and A100 cards for deep learning applications.", - "Purdue University RCAC" + "Purdue University RCAC", + "anviluser", + 22, + "SSH_KEY", + "/anvil/scratch", + "SLURM", + "SCP" ), new ComputeResource( @@ -129,7 +149,13 @@ private void initializeComputeResources() { "CentOS 7", "SLURM", "Leadership computing facility with specialized queues for different workload types. Maximum allocation: 3M core-hours.", - "Texas Advanced Computing Center" + "Texas Advanced Computing Center", + "fronterauser", + 22, + "SSH_KEY", + "/scratch1/projects", + "SLURM", + "SCP" ), new ComputeResource( @@ -142,7 +168,13 @@ private void initializeComputeResources() { "Amazon Linux 2", "Cloud Native", "Pay-as-you-go pricing model with various instance types (CPU, memory, GPU optimized). Global availability zones.", - "Amazon Web Services" + "Amazon Web Services", + "ec2-user", + 22, + "SSH_KEY", + "/home/ec2-user", + "Cloud Native", + "SCP" ), new ComputeResource( @@ -155,7 +187,13 @@ private void initializeComputeResources() { "Ubuntu 20.04 LTS", "Cloud Native", "Preemptible instances available for cost savings. TPUs available for machine learning acceleration.", - "Google Cloud Platform" + "Google Cloud Platform", + "gceuser", + 22, + "SSH_KEY", + "/home/gceuser", + "Cloud Native", + "SCP" ), new ComputeResource( @@ -168,7 +206,13 @@ private void initializeComputeResources() { "CentOS 7", "SLURM", "72 GPU nodes with K80 cards, high-speed interconnect, and parallel file systems for data-intensive research.", - "San Diego Supercomputer Center" + "San Diego Supercomputer Center", + "cometuser", + 22, + "SSH_KEY", + "/oasis/scratch/comet", + "SLURM", + "SCP" ), new ComputeResource( @@ -181,7 +225,13 @@ private void initializeComputeResources() { "Various Linux Distributions", "OpenStack", "Self-service cloud environment with support for containers, Kubernetes, and Jupyter notebooks.", - "Indiana University & TACC" + "Indiana University & TACC", + "jetstream", + 22, + "SSH_KEY", + "/home/jetstream", + "OpenStack", + "SCP" ), new ComputeResource( @@ -194,15 +244,140 @@ private void initializeComputeResources() { "SUSE Linux Enterprise", "SLURM", "A100 GPU nodes optimized for mixed-precision computing. Advanced interconnect and parallel file systems.", - "National Energy Research Scientific Computing Center" + "National Energy Research Scientific Computing Center", + "perlmutter", + 22, + "SSH_KEY", + "/global/cfs/cdirs", + "SLURM", + "SCP" ) ); computeResourceRepository.saveAll(computeResources); LOGGER.info("Created {} compute resources", computeResources.size()); + + // Initialize queues for each compute resource + initializeComputeResourceQueues(computeResources); } } + private void initializeComputeResourceQueues(List computeResources) { + LOGGER.info("Creating mock compute resource queues..."); + + for (ComputeResource computeResource : computeResources) { + List queues = new ArrayList<>(); + + // Create different queue configurations based on resource type + if (computeResource.getComputeType().equals("HPC")) { + // Standard HPC queues + queues.add(new ComputeResourceQueue( + "GPU queue", + "High-priority queue for GPU-accelerated workloads", + 2880, // 48 hours + 32, + 1024, + 100, + 64, + 1, + 64, + 60, // 1 hour default + "#SBATCH --partition=gpu\n#SBATCH --gres=gpu:4", + false + )); + + queues.add(new ComputeResourceQueue( + "Compute queue", + "Standard compute queue for CPU-intensive workloads", + 1440, // 24 hours + 128, + 2048, + 500, + 48, + 2, + 96, + 120, // 2 hours default + "#SBATCH --partition=compute", + true // Default queue + )); + + queues.add(new ComputeResourceQueue( + "Debug queue", + "Quick debug queue with shorter runtime limits", + 30, // 30 minutes + 4, + 128, + 10, + 24, + 1, + 24, + 15, // 15 minutes default + "#SBATCH --partition=debug\n#SBATCH --qos=debug", + false + )); + + queues.add(new ComputeResourceQueue( + "GPU shared queue", + "Shared GPU resources for smaller workloads", + 720, // 12 hours + 8, + 256, + 50, + 32, + 1, + 32, + 30, // 30 minutes default + "#SBATCH --partition=gpu-shared\n#SBATCH --gres=gpu:1", + false + )); + + } else if (computeResource.getComputeType().equals("Cloud")) { + // Cloud-based queues + queues.add(new ComputeResourceQueue( + "On-demand", + "On-demand instances with flexible resource allocation", + 10080, // 7 days + 1000, + 10000, + 1000, + 96, + 1, + 4, + 60, // 1 hour default + "# Cloud-native auto-scaling enabled", + true // Default queue + )); + + queues.add(new ComputeResourceQueue( + "Spot instances", + "Cost-optimized spot instances for fault-tolerant workloads", + 2880, // 48 hours + 500, + 5000, + 500, + 96, + 1, + 2, + 30, // 30 minutes default + "# Spot instance with automatic checkpointing", + false + )); + } + + // Set the compute resource relationship and save + for (ComputeResourceQueue queue : queues) { + queue.setComputeResource(computeResource); + } + + computeResource.setQueues(queues); + } + + // Save all compute resources with their queues + computeResourceRepository.saveAll(computeResources); + + LOGGER.info("Created queues for {} compute resources", computeResources.size()); + } + private void initializeStorageResources() { if (storageResourceRepository.count() == 0) { LOGGER.info("Creating mock storage resources..."); diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java index e70c8f990a2..bb54c6d2430 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java @@ -28,6 +28,7 @@ import org.apache.airavata.research.service.enums.StateEnum; import org.apache.airavata.research.service.v2.entity.ComputeResource; import org.apache.airavata.research.service.v2.repository.ComputeResourceRepository; +import org.apache.airavata.research.service.v2.service.ComputeResourceService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; @@ -57,9 +58,12 @@ public class ComputeResourceController { private static final StateEnum ACTIVE_STATE = StateEnum.ACTIVE; private final ComputeResourceRepository computeResourceRepository; + private final ComputeResourceService computeResourceService; - public ComputeResourceController(ComputeResourceRepository computeResourceRepository) { + public ComputeResourceController(ComputeResourceRepository computeResourceRepository, + ComputeResourceService computeResourceService) { this.computeResourceRepository = computeResourceRepository; + this.computeResourceService = computeResourceService; } @Operation(summary = "Get all public compute resources with pagination") @@ -76,9 +80,9 @@ public ResponseEntity> getComputeResources( Page resources; if (nameSearch != null && !nameSearch.trim().isEmpty()) { - resources = computeResourceRepository.findByNameSearchAndPrivacyAndState(nameSearch, PUBLIC_PRIVACY, ACTIVE_STATE, pageable); + resources = computeResourceService.searchComputeResources(nameSearch, PUBLIC_PRIVACY, ACTIVE_STATE, pageable); } else { - resources = computeResourceRepository.findByPrivacyAndState(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); + resources = computeResourceService.getComputeResources(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); } LOGGER.info("Found {} compute resources", resources.getTotalElements()); @@ -90,7 +94,7 @@ public ResponseEntity> getComputeResources( public ResponseEntity getComputeResourceById(@PathVariable("id") String id) { LOGGER.info("Getting compute resource by ID: {}", id); - Optional resource = computeResourceRepository.findById(id); + Optional resource = computeResourceService.getComputeResourceById(id); if (resource.isPresent()) { return ResponseEntity.ok(resource.get()); } else { @@ -130,7 +134,7 @@ public ResponseEntity createComputeResource(@Valid @RequestBody ComputeResour } // Note: starCount functionality handled separately in v1 star system - ComputeResource savedResource = computeResourceRepository.save(computeResource); + ComputeResource savedResource = computeResourceService.createComputeResource(computeResource); LOGGER.info("Created compute resource with ID: {}", savedResource.getId()); return ResponseEntity.status(HttpStatus.CREATED).body(savedResource); @@ -157,19 +161,13 @@ public ResponseEntity updateComputeResource(@PathVariable("id") String id, @V } try { - Optional existingResource = computeResourceRepository.findById(id); - if (!existingResource.isPresent()) { + Optional updatedResourceOpt = computeResourceService.updateComputeResource(id, computeResource); + if (!updatedResourceOpt.isPresent()) { LOGGER.warn("Compute resource not found with ID: {}", id); return ResponseEntity.notFound().build(); } - // Set the ID to ensure we update the correct resource - computeResource.setId(id); - - // Preserve creation timestamp - computeResource.setCreatedAt(existingResource.get().getCreatedAt()); - - ComputeResource updatedResource = computeResourceRepository.save(computeResource); + ComputeResource updatedResource = updatedResourceOpt.get(); LOGGER.info("Successfully updated compute resource with ID: {}", id); return ResponseEntity.ok(updatedResource); @@ -186,13 +184,12 @@ public ResponseEntity deleteComputeResource(@PathVariable("id") String id) { LOGGER.info("Deleting compute resource with ID: {}", id); try { - Optional existingResource = computeResourceRepository.findById(id); - if (!existingResource.isPresent()) { + boolean deleted = computeResourceService.deleteComputeResource(id); + if (!deleted) { LOGGER.warn("Compute resource not found with ID: {}", id); return ResponseEntity.notFound().build(); } - computeResourceRepository.deleteById(id); LOGGER.info("Successfully deleted compute resource with ID: {}", id); return ResponseEntity.ok().body("Compute resource deleted successfully"); } catch (Exception e) { @@ -236,7 +233,7 @@ public ResponseEntity starComputeResource(@PathVariable("id") String id LOGGER.info("Toggling star for compute resource with ID: {}", id); try { - Optional resourceOpt = computeResourceRepository.findById(id); + Optional resourceOpt = computeResourceRepository.findByIdWithCollections(id); if (resourceOpt.isPresent()) { ComputeResource resource = resourceOpt.get(); @@ -260,7 +257,7 @@ public ResponseEntity checkComputeResourceStarred(@PathVariable("id") S LOGGER.info("Checking if compute resource is starred: {}", id); try { - Optional resourceOpt = computeResourceRepository.findById(id); + Optional resourceOpt = computeResourceRepository.findByIdWithCollections(id); if (resourceOpt.isPresent()) { ComputeResource resource = resourceOpt.get(); // TODO: Implement proper v1 ResourceStar system integration @@ -282,7 +279,7 @@ public ResponseEntity getComputeResourceStarCount(@PathVariable("id") S LOGGER.info("Getting star count for compute resource: {}", id); try { - Optional resourceOpt = computeResourceRepository.findById(id); + Optional resourceOpt = computeResourceRepository.findByIdWithCollections(id); if (resourceOpt.isPresent()) { // TODO: Implement proper v1 ResourceStar system integration return ResponseEntity.ok(0); diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java index 25f8a507d11..08b01800dd0 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java @@ -22,12 +22,20 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.CascadeType; +import jakarta.persistence.FetchType; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import java.util.HashSet; import java.util.Set; +import java.util.List; +import java.util.ArrayList; import org.apache.airavata.research.service.enums.PrivacyEnum; import org.apache.airavata.research.service.enums.ResourceTypeEnum; import org.apache.airavata.research.service.enums.StateEnum; @@ -76,6 +84,54 @@ public class ComputeResource extends Resource { @Size(max = 255, message = "Resource manager must not exceed 255 characters") private String resourceManager; // Gateway name or organization + // New fields to match UI requirements + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable(name = "COMPUTE_RESOURCE_HOST_ALIASES", joinColumns = @JoinColumn(name = "compute_resource_id")) + @Column(name = "host_alias") + private List hostAliases = new ArrayList<>(); + + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable(name = "COMPUTE_RESOURCE_IP_ADDRESSES", joinColumns = @JoinColumn(name = "compute_resource_id")) + @Column(name = "ip_address") + private List ipAddresses = new ArrayList<>(); + + @Column(nullable = false) + @NotBlank(message = "SSH username is required") + @Size(max = 100, message = "SSH username must not exceed 100 characters") + private String sshUsername; + + @Column(nullable = false) + @NotNull(message = "SSH port is required") + @Min(value = 1, message = "SSH port must be at least 1") + private Integer sshPort; + + @Column(nullable = false) + @NotBlank(message = "Authentication method is required") + @Size(max = 50, message = "Authentication method must not exceed 50 characters") + private String authenticationMethod; // SSH_KEY or PASSWORD + + @Column(columnDefinition = "TEXT") + private String sshKey; // SSH key content for SSH_KEY authentication + + @Column(nullable = false) + @NotBlank(message = "Working directory is required") + @Size(max = 500, message = "Working directory must not exceed 500 characters") + private String workingDirectory; + + @Column(nullable = false) + @NotBlank(message = "Scheduler type is required") + @Size(max = 50, message = "Scheduler type must not exceed 50 characters") + private String schedulerType; // SLURM, PBS, SGE, etc. + + @Column(nullable = false) + @NotBlank(message = "Data movement protocol is required") + @Size(max = 50, message = "Data movement protocol must not exceed 50 characters") + private String dataMovementProtocol; // SCP, SFTP, etc. + + // One-to-many relationship with Queue entities + @OneToMany(mappedBy = "computeResource", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) + private List queues = new ArrayList<>(); + @Override public ResourceTypeEnum getType() { return ResourceTypeEnum.COMPUTE_RESOURCE; @@ -87,7 +143,9 @@ public ComputeResource() {} // Constructor for mock data creation public ComputeResource(String name, String description, String hostname, String computeType, Integer cpuCores, Integer memoryGB, String operatingSystem, - String queueSystem, String additionalInfo, String resourceManager) { + String queueSystem, String additionalInfo, String resourceManager, + String sshUsername, Integer sshPort, String authenticationMethod, + String workingDirectory, String schedulerType, String dataMovementProtocol) { this.setName(name); this.setDescription(description); this.hostname = hostname; @@ -98,6 +156,17 @@ public ComputeResource(String name, String description, String hostname, String this.queueSystem = queueSystem; this.additionalInfo = additionalInfo; this.resourceManager = resourceManager; + this.sshUsername = sshUsername; + this.sshPort = sshPort; + this.authenticationMethod = authenticationMethod; + this.workingDirectory = workingDirectory; + this.schedulerType = schedulerType; + this.dataMovementProtocol = dataMovementProtocol; + + // Initialize collections + this.hostAliases = new ArrayList<>(); + this.ipAddresses = new ArrayList<>(); + this.queues = new ArrayList<>(); // Set inherited v1 Resource fields (required) this.setPrivacy(PrivacyEnum.PUBLIC); @@ -172,4 +241,86 @@ public String getResourceManager() { public void setResourceManager(String resourceManager) { this.resourceManager = resourceManager; } + + // Getters and Setters for new fields + + public List getHostAliases() { + return hostAliases; + } + + public void setHostAliases(List hostAliases) { + this.hostAliases = hostAliases; + } + + public List getIpAddresses() { + return ipAddresses; + } + + public void setIpAddresses(List ipAddresses) { + this.ipAddresses = ipAddresses; + } + + public String getSshUsername() { + return sshUsername; + } + + public void setSshUsername(String sshUsername) { + this.sshUsername = sshUsername; + } + + public Integer getSshPort() { + return sshPort; + } + + public void setSshPort(Integer sshPort) { + this.sshPort = sshPort; + } + + public String getAuthenticationMethod() { + return authenticationMethod; + } + + public void setAuthenticationMethod(String authenticationMethod) { + this.authenticationMethod = authenticationMethod; + } + + public String getSshKey() { + return sshKey; + } + + public void setSshKey(String sshKey) { + this.sshKey = sshKey; + } + + public String getWorkingDirectory() { + return workingDirectory; + } + + public void setWorkingDirectory(String workingDirectory) { + this.workingDirectory = workingDirectory; + } + + public String getSchedulerType() { + return schedulerType; + } + + public void setSchedulerType(String schedulerType) { + this.schedulerType = schedulerType; + } + + public String getDataMovementProtocol() { + return dataMovementProtocol; + } + + public void setDataMovementProtocol(String dataMovementProtocol) { + this.dataMovementProtocol = dataMovementProtocol; + } + + public List getQueues() { + return queues; + } + + public void setQueues(List queues) { + this.queues = queues; + } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResourceQueue.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResourceQueue.java new file mode 100644 index 00000000000..737cf07e070 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResourceQueue.java @@ -0,0 +1,232 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.FetchType; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import com.fasterxml.jackson.annotation.JsonIgnore; + +@Entity +@Table(name = "COMPUTE_RESOURCE_QUEUE_V2") +public class ComputeResourceQueue { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + @NotBlank(message = "Queue name is required") + @Size(max = 100, message = "Queue name must not exceed 100 characters") + private String queueName; + + @Column(columnDefinition = "TEXT") + private String queueDescription; + + @Column + @Min(value = 1, message = "Queue max run time must be at least 1 minute") + private Integer queueMaxRunTime; // in minutes + + @Column + @Min(value = 1, message = "Queue max nodes must be at least 1") + private Integer queueMaxNodes; + + @Column + @Min(value = 1, message = "Queue max processors must be at least 1") + private Integer queueMaxProcessors; + + @Column + @Min(value = 1, message = "Max jobs in queue must be at least 1") + private Integer maxJobsInQueue; + + @Column + @Min(value = 1, message = "CPUs per node must be at least 1") + private Integer cpusPerNode; + + @Column + @Min(value = 1, message = "Default node count must be at least 1") + private Integer defaultNodeCount; + + @Column + @Min(value = 1, message = "Default CPU count must be at least 1") + private Integer defaultCpuCount; + + @Column + @Min(value = 1, message = "Default wall time must be at least 1 minute") + private Integer defaultWallTime; // in minutes + + @Column(columnDefinition = "TEXT") + private String queueSpecificMacros; + + @Column + private Boolean isDefaultQueue = false; + + // Many-to-one relationship with ComputeResource + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "compute_resource_id", nullable = false) + @JsonIgnore + private ComputeResource computeResource; + + // Default constructor + public ComputeResourceQueue() {} + + // Constructor for creating queue entries + public ComputeResourceQueue(String queueName, String queueDescription, Integer queueMaxRunTime, + Integer queueMaxNodes, Integer queueMaxProcessors, Integer maxJobsInQueue, + Integer cpusPerNode, Integer defaultNodeCount, Integer defaultCpuCount, + Integer defaultWallTime, String queueSpecificMacros, Boolean isDefaultQueue) { + this.queueName = queueName; + this.queueDescription = queueDescription; + this.queueMaxRunTime = queueMaxRunTime; + this.queueMaxNodes = queueMaxNodes; + this.queueMaxProcessors = queueMaxProcessors; + this.maxJobsInQueue = maxJobsInQueue; + this.cpusPerNode = cpusPerNode; + this.defaultNodeCount = defaultNodeCount; + this.defaultCpuCount = defaultCpuCount; + this.defaultWallTime = defaultWallTime; + this.queueSpecificMacros = queueSpecificMacros; + this.isDefaultQueue = isDefaultQueue != null ? isDefaultQueue : false; + } + + // Getters and Setters + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getQueueName() { + return queueName; + } + + public void setQueueName(String queueName) { + this.queueName = queueName; + } + + public String getQueueDescription() { + return queueDescription; + } + + public void setQueueDescription(String queueDescription) { + this.queueDescription = queueDescription; + } + + public Integer getQueueMaxRunTime() { + return queueMaxRunTime; + } + + public void setQueueMaxRunTime(Integer queueMaxRunTime) { + this.queueMaxRunTime = queueMaxRunTime; + } + + public Integer getQueueMaxNodes() { + return queueMaxNodes; + } + + public void setQueueMaxNodes(Integer queueMaxNodes) { + this.queueMaxNodes = queueMaxNodes; + } + + public Integer getQueueMaxProcessors() { + return queueMaxProcessors; + } + + public void setQueueMaxProcessors(Integer queueMaxProcessors) { + this.queueMaxProcessors = queueMaxProcessors; + } + + public Integer getMaxJobsInQueue() { + return maxJobsInQueue; + } + + public void setMaxJobsInQueue(Integer maxJobsInQueue) { + this.maxJobsInQueue = maxJobsInQueue; + } + + public Integer getCpusPerNode() { + return cpusPerNode; + } + + public void setCpusPerNode(Integer cpusPerNode) { + this.cpusPerNode = cpusPerNode; + } + + public Integer getDefaultNodeCount() { + return defaultNodeCount; + } + + public void setDefaultNodeCount(Integer defaultNodeCount) { + this.defaultNodeCount = defaultNodeCount; + } + + public Integer getDefaultCpuCount() { + return defaultCpuCount; + } + + public void setDefaultCpuCount(Integer defaultCpuCount) { + this.defaultCpuCount = defaultCpuCount; + } + + public Integer getDefaultWallTime() { + return defaultWallTime; + } + + public void setDefaultWallTime(Integer defaultWallTime) { + this.defaultWallTime = defaultWallTime; + } + + public String getQueueSpecificMacros() { + return queueSpecificMacros; + } + + public void setQueueSpecificMacros(String queueSpecificMacros) { + this.queueSpecificMacros = queueSpecificMacros; + } + + public Boolean getIsDefaultQueue() { + return isDefaultQueue; + } + + public void setIsDefaultQueue(Boolean isDefaultQueue) { + this.isDefaultQueue = isDefaultQueue; + } + + public ComputeResource getComputeResource() { + return computeResource; + } + + public void setComputeResource(ComputeResource computeResource) { + this.computeResource = computeResource; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java index eff227466fd..02cc90754bc 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java @@ -20,6 +20,7 @@ package org.apache.airavata.research.service.v2.repository; import java.util.List; +import java.util.Optional; import org.apache.airavata.research.service.v2.entity.ComputeResource; import org.apache.airavata.research.service.enums.PrivacyEnum; import org.apache.airavata.research.service.enums.StateEnum; @@ -54,4 +55,10 @@ Page findByNameSearchAndPrivacyAndState(@Param("nameSearch") St // Find all public and active resources List findAllByPrivacyAndState(PrivacyEnum privacy, StateEnum state); + + // Find by ID with eager fetching of queues only + @Query("SELECT DISTINCT c FROM ComputeResource c " + + "LEFT JOIN FETCH c.queues " + + "WHERE c.id = :id") + Optional findByIdWithCollections(@Param("id") String id); } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/service/ComputeResourceService.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/service/ComputeResourceService.java new file mode 100644 index 00000000000..2f7460e3b6b --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/service/ComputeResourceService.java @@ -0,0 +1,190 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.v2.service; + +import java.util.Optional; +import org.apache.airavata.research.service.v2.entity.ComputeResource; +import org.apache.airavata.research.service.v2.repository.ComputeResourceRepository; +import org.apache.airavata.research.service.enums.PrivacyEnum; +import org.apache.airavata.research.service.enums.StateEnum; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Service +@Transactional +public class ComputeResourceService { + + private static final Logger LOGGER = LoggerFactory.getLogger(ComputeResourceService.class); + + private final ComputeResourceRepository computeResourceRepository; + + public ComputeResourceService(ComputeResourceRepository computeResourceRepository) { + this.computeResourceRepository = computeResourceRepository; + } + + /** + * Get paginated compute resources with lazy collections properly initialized + */ + @Transactional(readOnly = true) + public Page getComputeResources(PrivacyEnum privacy, StateEnum state, Pageable pageable) { + LOGGER.debug("Fetching compute resources - privacy: {}, state: {}", privacy, state); + + Page resources = computeResourceRepository.findByPrivacyAndState(privacy, state, pageable); + + // Initialize lazy collections within transaction context + resources.getContent().forEach(this::initializeLazyCollections); + + return resources; + } + + /** + * Search compute resources with lazy collections properly initialized + */ + @Transactional(readOnly = true) + public Page searchComputeResources(String nameSearch, PrivacyEnum privacy, StateEnum state, Pageable pageable) { + LOGGER.debug("Searching compute resources - search: {}, privacy: {}, state: {}", nameSearch, privacy, state); + + Page resources = computeResourceRepository.findByNameSearchAndPrivacyAndState(nameSearch, privacy, state, pageable); + + // Initialize lazy collections within transaction context + resources.getContent().forEach(this::initializeLazyCollections); + + return resources; + } + + /** + * Get compute resource by ID with lazy collections properly initialized + */ + @Transactional(readOnly = true) + public Optional getComputeResourceById(String id) { + LOGGER.debug("Fetching compute resource by ID: {}", id); + + Optional resource = computeResourceRepository.findById(id); + + // Initialize lazy collections if resource exists + resource.ifPresent(this::initializeLazyCollections); + + return resource; + } + + /** + * Create new compute resource + */ + public ComputeResource createComputeResource(ComputeResource computeResource) { + LOGGER.debug("Creating compute resource: {}", computeResource.getName()); + + // Set any business logic defaults here if needed + ComputeResource savedResource = computeResourceRepository.save(computeResource); + + // Initialize collections for return + initializeLazyCollections(savedResource); + + return savedResource; + } + + /** + * Update existing compute resource + */ + public Optional updateComputeResource(String id, ComputeResource updatedResource) { + LOGGER.debug("Updating compute resource ID: {}", id); + + Optional existingResource = computeResourceRepository.findById(id); + + if (existingResource.isPresent()) { + ComputeResource resource = existingResource.get(); + + // Update fields + resource.setName(updatedResource.getName()); + resource.setDescription(updatedResource.getDescription()); + resource.setHostname(updatedResource.getHostname()); + resource.setComputeType(updatedResource.getComputeType()); + resource.setCpuCores(updatedResource.getCpuCores()); + resource.setMemoryGB(updatedResource.getMemoryGB()); + resource.setOperatingSystem(updatedResource.getOperatingSystem()); + resource.setQueueSystem(updatedResource.getQueueSystem()); + resource.setResourceManager(updatedResource.getResourceManager()); + resource.setAdditionalInfo(updatedResource.getAdditionalInfo()); + + // Update new fields + resource.setHostAliases(updatedResource.getHostAliases()); + resource.setIpAddresses(updatedResource.getIpAddresses()); + resource.setSshUsername(updatedResource.getSshUsername()); + resource.setSshPort(updatedResource.getSshPort()); + resource.setAuthenticationMethod(updatedResource.getAuthenticationMethod()); + resource.setSshKey(updatedResource.getSshKey()); + resource.setWorkingDirectory(updatedResource.getWorkingDirectory()); + resource.setSchedulerType(updatedResource.getSchedulerType()); + resource.setDataMovementProtocol(updatedResource.getDataMovementProtocol()); + resource.setQueues(updatedResource.getQueues()); + + ComputeResource savedResource = computeResourceRepository.save(resource); + + // Initialize collections for return + initializeLazyCollections(savedResource); + + return Optional.of(savedResource); + } + + return Optional.empty(); + } + + /** + * Delete compute resource + */ + public boolean deleteComputeResource(String id) { + LOGGER.debug("Deleting compute resource ID: {}", id); + + if (computeResourceRepository.existsById(id)) { + computeResourceRepository.deleteById(id); + return true; + } + + return false; + } + + /** + * Initialize lazy collections within transaction context to avoid LazyInitializationException + */ + private void initializeLazyCollections(ComputeResource resource) { + try { + // Force initialization of lazy collections + if (resource.getHostAliases() != null) { + resource.getHostAliases().size(); // Trigger lazy loading + } + if (resource.getIpAddresses() != null) { + resource.getIpAddresses().size(); // Trigger lazy loading + } + if (resource.getQueues() != null) { + resource.getQueues().size(); // Trigger lazy loading + // Also initialize queue details if needed + resource.getQueues().forEach(queue -> { + // Access queue properties to ensure they're loaded + queue.getQueueName(); + }); + } + } catch (Exception e) { + LOGGER.warn("Failed to initialize lazy collections for compute resource {}: {}", resource.getId(), e.getMessage()); + } + } +} \ No newline at end of file From 33d4ff491ada6f25726bd39a1584351749403a33 Mon Sep 17 00:00:00 2001 From: Krish Katariya Date: Thu, 31 Jul 2025 21:18:10 -0700 Subject: [PATCH 08/17] Replicating airavata-api structure for compute and storage resources --- .../config/LocalResourceDataInitializer.java | 278 +++++++ .../service/config/RegistryServiceConfig.java | 143 ++++ .../ComputeResourceDTO.java} | 229 +++-- .../service/dto/ComputeResourceQueueDTO.java | 185 +++++ .../StorageResourceDTO.java} | 253 +++--- .../entity/LocalComputeResourceEntity.java | 179 ++++ .../entity/LocalStorageResourceEntity.java | 124 +++ .../handler/ComputeResourceHandler.java | 269 ++++++ .../handler/LocalComputeResourceHandler.java | 230 ++++++ .../handler/LocalStorageResourceHandler.java | 257 ++++++ .../handler/StorageResourceHandler.java | 300 +++++++ .../service/model/entity/StorageResource.java | 60 ++ .../model/repo/StorageResourceRepository.java | 1 + .../LocalComputeResourceRepository.java | 55 ++ .../LocalStorageResourceRepository.java | 55 ++ .../research/service/util/DTOConverter.java | 781 ++++++++++++++++++ .../service/v2/config/V2DataInitializer.java | 765 ++++------------- .../controller/ComputeResourceController.java | 175 ++-- .../controller/StorageResourceController.java | 180 ++-- .../research/service/v2/entity/Code.java | 36 + .../v2/entity/ComputeResourceQueue.java | 232 ------ .../repository/ComputeResourceRepository.java | 64 -- .../repository/StorageResourceRepository.java | 57 -- .../v2/service/ComputeResourceService.java | 190 ----- .../src/main/resources/application.yml | 4 + 25 files changed, 3493 insertions(+), 1609 deletions(-) create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/LocalResourceDataInitializer.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/RegistryServiceConfig.java rename modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/{v2/entity/ComputeResource.java => dto/ComputeResourceDTO.java} (53%) create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/ComputeResourceQueueDTO.java rename modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/{v2/entity/StorageResource.java => dto/StorageResourceDTO.java} (56%) create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/LocalComputeResourceEntity.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/LocalStorageResourceEntity.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/ComputeResourceHandler.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/LocalComputeResourceHandler.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/LocalStorageResourceHandler.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/StorageResourceHandler.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/StorageResource.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/StorageResourceRepository.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/LocalComputeResourceRepository.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/LocalStorageResourceRepository.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResourceQueue.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/StorageResourceRepository.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/service/ComputeResourceService.java diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/LocalResourceDataInitializer.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/LocalResourceDataInitializer.java new file mode 100644 index 00000000000..f9a1d8f8f26 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/LocalResourceDataInitializer.java @@ -0,0 +1,278 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; +import java.sql.Timestamp; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import org.apache.airavata.research.service.entity.LocalComputeResourceEntity; +import org.apache.airavata.research.service.entity.LocalStorageResourceEntity; +import org.apache.airavata.research.service.repository.LocalComputeResourceRepository; +import org.apache.airavata.research.service.repository.LocalStorageResourceRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * Local data initializer using existing airavata-api entities + * Generates sample data for development without external registry services + */ +@Component +public class LocalResourceDataInitializer { + + private static final Logger LOGGER = LoggerFactory.getLogger(LocalResourceDataInitializer.class); + private final ObjectMapper objectMapper = new ObjectMapper(); + + private final LocalStorageResourceRepository storageResourceRepository; + private final LocalComputeResourceRepository computeResourceRepository; + + public LocalResourceDataInitializer(LocalStorageResourceRepository storageResourceRepository, + LocalComputeResourceRepository computeResourceRepository) { + this.storageResourceRepository = storageResourceRepository; + this.computeResourceRepository = computeResourceRepository; + } + + @PostConstruct + public void initializeData() { + LOGGER.info("Initializing local resource data using airavata-api entities..."); + + try { + initializeStorageResources(); + initializeComputeResources(); + + LOGGER.info("Local resource data initialization completed."); + } catch (Exception e) { + LOGGER.error("Error during local resource data initialization: {}", e.getMessage(), e); + throw new RuntimeException("Failed to initialize local resource data", e); + } + } + + private void initializeStorageResources() { + if (storageResourceRepository.count() == 0) { + LOGGER.info("Creating local storage resources using LocalStorageResourceEntity..."); + + LocalStorageResourceEntity[] storageResources = { + createS3StorageResource( + "HPC Research Data Lake", + "s3.research.university.edu", + "Large-scale S3-compatible storage for computational research data with 500TB capacity", + createS3UIFields("hpc-research-data-lake", "us-east-1", 500L) + ), + + createS3StorageResource( + "Genomics Cloud Storage", + "genomics-s3.cloud.edu", + "Specialized S3 storage for genomics research data with HIPAA compliance", + createS3UIFields("genomics-research-bucket", "us-west-2", 1000L) + ), + + createS3StorageResource( + "Neural Network Model Archive", + "ml-models.storage.edu", + "S3 storage optimized for deep learning model artifacts and training datasets", + createS3UIFields("neural-network-models", "us-east-1", 250L) + ), + + createSCPStorageResource( + "Supercomputer Scratch Storage", + "hpc-cluster.university.edu", + "High-performance parallel filesystem on supercomputing cluster for active computations", + createSCPUIFields(22, "research_user", "/scratch/research_projects", 2000L) + ), + + createSCPStorageResource( + "Lab Server Archive", + "lab-server.research.university.edu", + "Local lab server storage for secure research data archival and backup", + createSCPUIFields(2222, "lab_admin", "/data/archive", 50L) + ), + + createSCPStorageResource( + "Collaboration Storage Server", + "collab-storage.consortium.org", + "Shared storage server for multi-institutional research collaboration", + createSCPUIFields(22, "collab_user", "/shared/projects", 100L) + ) + }; + + for (LocalStorageResourceEntity storage : storageResources) { + storageResourceRepository.save(storage); + } + + LOGGER.info("Created {} local storage resources", storageResources.length); + } + } + + private void initializeComputeResources() { + if (computeResourceRepository.count() == 0) { + LOGGER.info("Creating local compute resources using LocalComputeResourceEntity..."); + + LocalComputeResourceEntity[] computeResources = { + createComputeResource( + "Anvil Supercomputer (CPU)", + "anvil.rcac.purdue.edu", + "NSF-funded supercomputer at Purdue University with CPU nodes for large-scale scientific computing", + 128, 4, 256, 30, + createComputeUIFields("ANVIL_CPU", "CentOS_7", "SLURM", "SCP") + ), + + createComputeResource( + "Anvil Supercomputer (GPU)", + "anvil-gpu.rcac.purdue.edu", + "NSF-funded supercomputer at Purdue University with GPU nodes for AI/ML workloads", + 128, 4, 256, 30, + createComputeUIFields("ANVIL_GPU", "CentOS_7", "SLURM", "SCP") + ), + + createComputeResource( + "Bridges-2 (PSC)", + "bridges2.psc.edu", + "NSF-funded supercomputer at Pittsburgh Supercomputing Center for diverse research workloads", + 128, 8, 512, 48, + createComputeUIFields("BRIDGES2", "CentOS_7", "SLURM", "SCP") + ), + + createComputeResource( + "Expanse (SDSC)", + "login.expanse.sdsc.edu", + "NSF-funded supercomputer at San Diego Supercomputer Center for computational research", + 128, 2, 256, 48, + createComputeUIFields("EXPANSE", "CentOS_8", "SLURM", "SCP") + ), + + createComputeResource( + "Delta GPU Cluster", + "login.delta.ncsa.illinois.edu", + "GPU-focused supercomputer at NCSA for AI, machine learning, and data science workloads", + 64, 8, 256, 30, + createComputeUIFields("DELTA_GPU", "Red_Hat_8", "SLURM", "SCP") + ), + + createComputeResource( + "Stampede3 (TACC)", + "stampede3.tacc.utexas.edu", + "Advanced supercomputer at Texas Advanced Computing Center for high-performance computing", + 96, 4, 192, 48, + createComputeUIFields("STAMPEDE3", "CentOS_7", "SLURM", "SCP") + ) + }; + + for (LocalComputeResourceEntity compute : computeResources) { + computeResourceRepository.save(compute); + } + + LOGGER.info("Created {} local compute resources", computeResources.length); + } + } + + private LocalStorageResourceEntity createS3StorageResource(String description, String hostName, + String fullDescription, String uiFieldsJson) { + LocalStorageResourceEntity storage = new LocalStorageResourceEntity(); + storage.setStorageResourceId(UUID.randomUUID().toString()); + storage.setStorageResourceDescription(fullDescription + "\n\nUI_FIELDS: " + uiFieldsJson); + storage.setHostName(hostName); + storage.setEnabled(true); + storage.setCreationTime(new Timestamp(System.currentTimeMillis())); + storage.setUpdateTime(new Timestamp(System.currentTimeMillis())); + return storage; + } + + private LocalStorageResourceEntity createSCPStorageResource(String description, String hostName, + String fullDescription, String uiFieldsJson) { + LocalStorageResourceEntity storage = new LocalStorageResourceEntity(); + storage.setStorageResourceId(UUID.randomUUID().toString()); + storage.setStorageResourceDescription(fullDescription + "\n\nUI_FIELDS: " + uiFieldsJson); + storage.setHostName(hostName); + storage.setEnabled(true); + storage.setCreationTime(new Timestamp(System.currentTimeMillis())); + storage.setUpdateTime(new Timestamp(System.currentTimeMillis())); + return storage; + } + + private LocalComputeResourceEntity createComputeResource(String description, String hostName, String fullDescription, + int cpusPerNode, int defaultNodeCount, int maxMemoryPerNode, + int defaultWalltime, String uiFieldsJson) { + LocalComputeResourceEntity compute = new LocalComputeResourceEntity(); + compute.setComputeResourceId(UUID.randomUUID().toString()); + compute.setResourceDescription(fullDescription + "\n\nUI_FIELDS: " + uiFieldsJson); + compute.setHostName(hostName); + compute.setEnabled((short) 1); + compute.setCpusPerNode(cpusPerNode); + compute.setDefaultNodeCount(defaultNodeCount); + compute.setDefaultCPUCount(cpusPerNode * defaultNodeCount); + compute.setMaxMemoryPerNode(maxMemoryPerNode); + compute.setDefaultWalltime(defaultWalltime); + compute.setCreationTime(new Timestamp(System.currentTimeMillis())); + compute.setUpdateTime(new Timestamp(System.currentTimeMillis())); + return compute; + } + + // Helper methods to create UI-specific field JSON + private String createS3UIFields(String bucketName, String region, Long capacityTB) { + try { + Map uiFields = new HashMap<>(); + uiFields.put("storageType", "S3"); + uiFields.put("accessProtocol", "S3"); + uiFields.put("bucketName", bucketName); + uiFields.put("region", region); + uiFields.put("capacityTB", capacityTB); + uiFields.put("supportsEncryption", true); + uiFields.put("supportsVersioning", true); + return objectMapper.writeValueAsString(uiFields); + } catch (Exception e) { + LOGGER.warn("Failed to serialize S3 UI fields", e); + return "{}"; + } + } + + private String createSCPUIFields(Integer port, String username, String remotePath, Long capacityTB) { + try { + Map uiFields = new HashMap<>(); + uiFields.put("storageType", "SCP"); + uiFields.put("accessProtocol", "SCP"); + uiFields.put("port", port); + uiFields.put("username", username); + uiFields.put("remotePath", remotePath); + uiFields.put("capacityTB", capacityTB); + uiFields.put("authenticationMethod", "SSH_KEY"); + return objectMapper.writeValueAsString(uiFields); + } catch (Exception e) { + LOGGER.warn("Failed to serialize SCP UI fields", e); + return "{}"; + } + } + + private String createComputeUIFields(String computeType, String operatingSystem, String schedulerType, String dataMovementProtocol) { + try { + Map uiFields = new HashMap<>(); + uiFields.put("computeType", computeType); + uiFields.put("operatingSystem", operatingSystem); + uiFields.put("schedulerType", schedulerType); + uiFields.put("dataMovementProtocol", dataMovementProtocol); + return objectMapper.writeValueAsString(uiFields); + } catch (Exception e) { + LOGGER.warn("Failed to serialize compute UI fields", e); + return "{}"; + } + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/RegistryServiceConfig.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/RegistryServiceConfig.java new file mode 100644 index 00000000000..58befaf029b --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/RegistryServiceConfig.java @@ -0,0 +1,143 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.airavata.research.service.config; + +import org.apache.airavata.common.exception.ApplicationSettingsException; +import org.apache.airavata.common.utils.ServerSettings; +import org.apache.airavata.registry.api.RegistryService; +import org.apache.airavata.registry.api.client.RegistryServiceClientFactory; +import org.apache.airavata.registry.api.exception.RegistryServiceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for Airavata Registry Service integration + * Provides RegistryService client bean for accessing existing airavata-api infrastructure + * Uses lazy initialization to allow application startup even when registry service is unavailable + */ +@Configuration +public class RegistryServiceConfig { + + private static final Logger LOGGER = LoggerFactory.getLogger(RegistryServiceConfig.class); + + @Value("${airavata.registry.host:localhost}") + private String registryHost; + + @Value("${airavata.registry.port:9930}") + private int registryPort; + + @Value("${airavata.registry.enabled:true}") + private boolean registryEnabled; + + /** + * Creates RegistryService.Iface client bean using Airavata's RegistryServiceClientFactory + * This integrates with existing airavata-api infrastructure + * Uses lazy initialization to allow application startup without registry service + */ + @Bean + public RegistryServiceProvider registryServiceProvider() { + return new RegistryServiceProvider(); + } + + /** + * Provider class that handles lazy initialization and graceful failure of RegistryService + */ + public class RegistryServiceProvider { + private volatile RegistryService.Iface registryService; + private volatile boolean connectionAttempted = false; + private volatile Exception lastException; + + public RegistryService.Iface getRegistryService() throws RegistryServiceException { + if (!registryEnabled) { + throw new RegistryServiceException("Registry service is disabled. Enable with airavata.registry.enabled=true"); + } + + if (registryService == null && !connectionAttempted) { + synchronized (this) { + if (registryService == null && !connectionAttempted) { + connectionAttempted = true; + try { + registryService = createRegistryService(); + LOGGER.info("Successfully connected to Airavata Registry Service"); + } catch (Exception e) { + lastException = e; + LOGGER.error("Failed to connect to Airavata Registry Service at {}:{} - {}", + registryHost, registryPort, e.getMessage()); + } + } + } + } + + if (registryService == null) { + String errorMsg = String.format("Registry service unavailable at %s:%d", registryHost, registryPort); + if (lastException != null) { + errorMsg += " - " + lastException.getMessage(); + } + throw new RegistryServiceException(errorMsg); + } + + return registryService; + } + + private RegistryService.Iface createRegistryService() throws RegistryServiceException { + String serverHost = getRegistryServerHost(); + int serverPort = getRegistryServerPort(); + + LOGGER.info("Attempting to connect to Airavata Registry Service at {}:{}", serverHost, serverPort); + + RegistryService.Client registryClient = RegistryServiceClientFactory.createRegistryClient(serverHost, serverPort); + return registryClient; // RegistryService.Client implements RegistryService.Iface + } + + public boolean isAvailable() { + try { + return getRegistryService() != null; + } catch (RegistryServiceException e) { + return false; + } + } + } + + /** + * Get registry server host from Airavata ServerSettings or fallback to application properties + */ + private String getRegistryServerHost() { + try { + return ServerSettings.getRegistryServerHost(); + } catch (ApplicationSettingsException e) { + LOGGER.warn("Unable to get registry host from ServerSettings, using configured value: {}", registryHost); + return registryHost; + } + } + + /** + * Get registry server port from Airavata ServerSettings or fallback to application properties + */ + private int getRegistryServerPort() { + try { + return Integer.parseInt(ServerSettings.getRegistryServerPort()); + } catch (ApplicationSettingsException | NumberFormatException e) { + LOGGER.warn("Unable to get registry port from ServerSettings, using configured value: {}", registryPort); + return registryPort; + } + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/ComputeResourceDTO.java similarity index 53% rename from modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java rename to modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/ComputeResourceDTO.java index 08b01800dd0..e5c09eba61f 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResource.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/ComputeResourceDTO.java @@ -1,189 +1,160 @@ /** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import jakarta.persistence.ElementCollection; -import jakarta.persistence.CollectionTable; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToMany; -import jakarta.persistence.CascadeType; -import jakarta.persistence.FetchType; + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.airavata.research.service.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; -import java.util.HashSet; -import java.util.Set; -import java.util.List; import java.util.ArrayList; -import org.apache.airavata.research.service.enums.PrivacyEnum; -import org.apache.airavata.research.service.enums.ResourceTypeEnum; -import org.apache.airavata.research.service.enums.StateEnum; -import org.apache.airavata.research.service.enums.StatusEnum; -import org.apache.airavata.research.service.model.entity.Resource; - -@Entity -@Table(name = "COMPUTE_RESOURCE_V2") -public class ComputeResource extends Resource { +import java.util.List; - @Column(nullable = false) +/** + * UI-specific DTO for Compute Resource + * Maps to airavata-api ComputeResourceDescription with UI-specific extensions + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ComputeResourceDTO { + + // Core fields from ComputeResourceDescription + private String computeResourceId; + + @NotBlank(message = "Compute resource name is required") + @Size(max = 255, message = "Compute resource name must not exceed 255 characters") + private String name; + + @NotBlank(message = "Resource description is required") + @Size(max = 1000, message = "Resource description must not exceed 1000 characters") + private String resourceDescription; + @NotBlank(message = "Hostname is required") @Size(max = 255, message = "Hostname must not exceed 255 characters") - private String hostname; + private String hostName; - @Column(nullable = false) + // UI-specific extensions stored in resourceDescription as JSON @NotBlank(message = "Compute type is required") @Size(max = 100, message = "Compute type must not exceed 100 characters") private String computeType; // HPC, Cloud, Local, etc. - @Column(nullable = false) @NotNull(message = "CPU cores is required") @Min(value = 1, message = "CPU cores must be at least 1") private Integer cpuCores; - @Column(nullable = false) @NotNull(message = "Memory GB is required") @Min(value = 1, message = "Memory GB must be at least 1") private Integer memoryGB; - @Column(nullable = false) @NotBlank(message = "Operating system is required") @Size(max = 100, message = "Operating system must not exceed 100 characters") private String operatingSystem; - @Column(nullable = false) @NotBlank(message = "Queue system is required") @Size(max = 100, message = "Queue system must not exceed 100 characters") private String queueSystem; // SLURM, PBS, SGE, etc. - @Column(columnDefinition = "TEXT") private String additionalInfo; - @Column(nullable = false) @NotBlank(message = "Resource manager is required") @Size(max = 255, message = "Resource manager must not exceed 255 characters") private String resourceManager; // Gateway name or organization - // New fields to match UI requirements - @ElementCollection(fetch = FetchType.LAZY) - @CollectionTable(name = "COMPUTE_RESOURCE_HOST_ALIASES", joinColumns = @JoinColumn(name = "compute_resource_id")) - @Column(name = "host_alias") + // Direct mappings from ComputeResourceDescription private List hostAliases = new ArrayList<>(); - - @ElementCollection(fetch = FetchType.LAZY) - @CollectionTable(name = "COMPUTE_RESOURCE_IP_ADDRESSES", joinColumns = @JoinColumn(name = "compute_resource_id")) - @Column(name = "ip_address") private List ipAddresses = new ArrayList<>(); - @Column(nullable = false) + // UI-specific SSH configuration fields @NotBlank(message = "SSH username is required") @Size(max = 100, message = "SSH username must not exceed 100 characters") private String sshUsername; - @Column(nullable = false) @NotNull(message = "SSH port is required") @Min(value = 1, message = "SSH port must be at least 1") private Integer sshPort; - @Column(nullable = false) @NotBlank(message = "Authentication method is required") @Size(max = 50, message = "Authentication method must not exceed 50 characters") private String authenticationMethod; // SSH_KEY or PASSWORD - @Column(columnDefinition = "TEXT") private String sshKey; // SSH key content for SSH_KEY authentication - @Column(nullable = false) @NotBlank(message = "Working directory is required") @Size(max = 500, message = "Working directory must not exceed 500 characters") private String workingDirectory; - @Column(nullable = false) @NotBlank(message = "Scheduler type is required") @Size(max = 50, message = "Scheduler type must not exceed 50 characters") private String schedulerType; // SLURM, PBS, SGE, etc. - @Column(nullable = false) @NotBlank(message = "Data movement protocol is required") @Size(max = 50, message = "Data movement protocol must not exceed 50 characters") private String dataMovementProtocol; // SCP, SFTP, etc. - // One-to-many relationship with Queue entities - @OneToMany(mappedBy = "computeResource", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) - private List queues = new ArrayList<>(); + // Queue management + private List queues = new ArrayList<>(); - @Override - public ResourceTypeEnum getType() { - return ResourceTypeEnum.COMPUTE_RESOURCE; - } + // System fields + private boolean enabled = true; + private Long creationTime; + private Long updateTime; // Default constructor - public ComputeResource() {} - - // Constructor for mock data creation - public ComputeResource(String name, String description, String hostname, String computeType, - Integer cpuCores, Integer memoryGB, String operatingSystem, - String queueSystem, String additionalInfo, String resourceManager, - String sshUsername, Integer sshPort, String authenticationMethod, - String workingDirectory, String schedulerType, String dataMovementProtocol) { - this.setName(name); - this.setDescription(description); - this.hostname = hostname; - this.computeType = computeType; - this.cpuCores = cpuCores; - this.memoryGB = memoryGB; - this.operatingSystem = operatingSystem; - this.queueSystem = queueSystem; - this.additionalInfo = additionalInfo; - this.resourceManager = resourceManager; - this.sshUsername = sshUsername; - this.sshPort = sshPort; - this.authenticationMethod = authenticationMethod; - this.workingDirectory = workingDirectory; - this.schedulerType = schedulerType; - this.dataMovementProtocol = dataMovementProtocol; - - // Initialize collections - this.hostAliases = new ArrayList<>(); - this.ipAddresses = new ArrayList<>(); - this.queues = new ArrayList<>(); - - // Set inherited v1 Resource fields (required) - this.setPrivacy(PrivacyEnum.PUBLIC); - this.setState(StateEnum.ACTIVE); - this.setStatus(StatusEnum.VERIFIED); - this.setAuthors(new HashSet<>()); - this.setTags(new HashSet<>()); - this.setHeaderImage(""); // Default empty header image + public ComputeResourceDTO() {} + + // Constructor for mapping from existing data + public ComputeResourceDTO(String computeResourceId, String hostName, String resourceDescription) { + this.computeResourceId = computeResourceId; + this.hostName = hostName; + this.resourceDescription = resourceDescription; } - // Getters and Setters for ComputeResource-specific fields - public String getHostname() { - return hostname; + // Getters and Setters + public String getComputeResourceId() { + return computeResourceId; } - public void setHostname(String hostname) { - this.hostname = hostname; + public void setComputeResourceId(String computeResourceId) { + this.computeResourceId = computeResourceId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getResourceDescription() { + return resourceDescription; + } + + public void setResourceDescription(String resourceDescription) { + this.resourceDescription = resourceDescription; + } + + public String getHostName() { + return hostName; + } + + public void setHostName(String hostName) { + this.hostName = hostName; } public String getComputeType() { @@ -242,8 +213,6 @@ public void setResourceManager(String resourceManager) { this.resourceManager = resourceManager; } - // Getters and Setters for new fields - public List getHostAliases() { return hostAliases; } @@ -316,11 +285,35 @@ public void setDataMovementProtocol(String dataMovementProtocol) { this.dataMovementProtocol = dataMovementProtocol; } - public List getQueues() { + public List getQueues() { return queues; } - public void setQueues(List queues) { + public void setQueues(List queues) { this.queues = queues; } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Long getCreationTime() { + return creationTime; + } + + public void setCreationTime(Long creationTime) { + this.creationTime = creationTime; + } + + public Long getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Long updateTime) { + this.updateTime = updateTime; + } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/ComputeResourceQueueDTO.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/ComputeResourceQueueDTO.java new file mode 100644 index 00000000000..ce89fe495a8 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/ComputeResourceQueueDTO.java @@ -0,0 +1,185 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.airavata.research.service.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +/** + * UI-specific DTO for Compute Resource Queue + * Maps directly to airavata-api BatchQueue model + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ComputeResourceQueueDTO { + + @NotBlank(message = "Queue name is required") + @Size(max = 255, message = "Queue name must not exceed 255 characters") + private String queueName; + + @Size(max = 1000, message = "Queue description must not exceed 1000 characters") + private String queueDescription; + + @Min(value = 1, message = "Max run time must be at least 1 hour") + private Integer maxRunTime; + + @Min(value = 1, message = "Max nodes must be at least 1") + private Integer maxNodes; + + @Min(value = 1, message = "Max processors must be at least 1") + private Integer maxProcessors; + + @Min(value = 1, message = "Max jobs in queue must be at least 1") + private Integer maxJobsInQueue; + + @Min(value = 1, message = "Max memory must be at least 1") + private Integer maxMemory; + + @Min(value = 1, message = "CPUs per node must be at least 1") + private Integer cpusPerNode; + + @Min(value = 1, message = "Default node count must be at least 1") + private Integer defaultNodeCount; + + @Min(value = 1, message = "Default CPU count must be at least 1") + private Integer defaultCpuCount; + + @Min(value = 1, message = "Default wall time must be at least 1") + private Integer defaultWallTime; + + @Size(max = 1000, message = "Queue specific macros must not exceed 1000 characters") + private String queueSpecificMacros; + + private Boolean isDefaultQueue = false; + + // Default constructor + public ComputeResourceQueueDTO() {} + + // Constructor for quick creation + public ComputeResourceQueueDTO(String queueName, String queueDescription) { + this.queueName = queueName; + this.queueDescription = queueDescription; + } + + // Getters and Setters + public String getQueueName() { + return queueName; + } + + public void setQueueName(String queueName) { + this.queueName = queueName; + } + + public String getQueueDescription() { + return queueDescription; + } + + public void setQueueDescription(String queueDescription) { + this.queueDescription = queueDescription; + } + + public Integer getMaxRunTime() { + return maxRunTime; + } + + public void setMaxRunTime(Integer maxRunTime) { + this.maxRunTime = maxRunTime; + } + + public Integer getMaxNodes() { + return maxNodes; + } + + public void setMaxNodes(Integer maxNodes) { + this.maxNodes = maxNodes; + } + + public Integer getMaxProcessors() { + return maxProcessors; + } + + public void setMaxProcessors(Integer maxProcessors) { + this.maxProcessors = maxProcessors; + } + + public Integer getMaxJobsInQueue() { + return maxJobsInQueue; + } + + public void setMaxJobsInQueue(Integer maxJobsInQueue) { + this.maxJobsInQueue = maxJobsInQueue; + } + + public Integer getMaxMemory() { + return maxMemory; + } + + public void setMaxMemory(Integer maxMemory) { + this.maxMemory = maxMemory; + } + + public Integer getCpusPerNode() { + return cpusPerNode; + } + + public void setCpusPerNode(Integer cpusPerNode) { + this.cpusPerNode = cpusPerNode; + } + + public Integer getDefaultNodeCount() { + return defaultNodeCount; + } + + public void setDefaultNodeCount(Integer defaultNodeCount) { + this.defaultNodeCount = defaultNodeCount; + } + + public Integer getDefaultCpuCount() { + return defaultCpuCount; + } + + public void setDefaultCpuCount(Integer defaultCpuCount) { + this.defaultCpuCount = defaultCpuCount; + } + + public Integer getDefaultWallTime() { + return defaultWallTime; + } + + public void setDefaultWallTime(Integer defaultWallTime) { + this.defaultWallTime = defaultWallTime; + } + + public String getQueueSpecificMacros() { + return queueSpecificMacros; + } + + public void setQueueSpecificMacros(String queueSpecificMacros) { + this.queueSpecificMacros = queueSpecificMacros; + } + + public Boolean getIsDefaultQueue() { + return isDefaultQueue; + } + + public void setIsDefaultQueue(Boolean isDefaultQueue) { + this.isDefaultQueue = isDefaultQueue; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/StorageResourceDTO.java similarity index 56% rename from modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java rename to modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/StorageResourceDTO.java index 125eef1bfdb..2f69f256e2c 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/StorageResource.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/StorageResourceDTO.java @@ -1,176 +1,179 @@ /** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.airavata.research.service.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; -import java.util.HashSet; -import org.apache.airavata.research.service.enums.PrivacyEnum; -import org.apache.airavata.research.service.enums.ResourceTypeEnum; -import org.apache.airavata.research.service.enums.StateEnum; -import org.apache.airavata.research.service.enums.StatusEnum; -import org.apache.airavata.research.service.model.entity.Resource; - -@Entity -@Table(name = "STORAGE_RESOURCE_V2") -public class StorageResource extends Resource { - - @Column(nullable = false) + +/** + * UI-specific DTO for Storage Resource + * Maps to airavata-api StorageResourceDescription with UI-specific extensions + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class StorageResourceDTO { + + // Core fields from StorageResourceDescription + private String storageResourceId; + + @NotBlank(message = "Storage resource name is required") + @Size(max = 255, message = "Storage resource name must not exceed 255 characters") + private String name; + @NotBlank(message = "Hostname is required") @Size(max = 255, message = "Hostname must not exceed 255 characters") - private String hostname; + private String hostName; + + @Size(max = 1000, message = "Storage resource description must not exceed 1000 characters") + private String storageResourceDescription; - @Column(nullable = false) + // UI-specific extensions stored in storageResourceDescription as JSON @NotBlank(message = "Storage type is required") @Size(max = 100, message = "Storage type must not exceed 100 characters") private String storageType; // S3, SCP, NFS, etc. - @Column(nullable = false) @NotNull(message = "Capacity TB is required") @Min(value = 1, message = "Capacity TB must be at least 1") private Long capacityTB; - @Column(nullable = false) @NotBlank(message = "Access protocol is required") @Size(max = 100, message = "Access protocol must not exceed 100 characters") private String accessProtocol; // S3, SFTP, NFS, HTTP, etc. - @Column(nullable = false) @NotBlank(message = "Endpoint is required") @Size(max = 500, message = "Endpoint must not exceed 500 characters") private String endpoint; // API endpoint or mount point - @Column(nullable = false) private Boolean supportsEncryption = false; - - @Column(nullable = false) private Boolean supportsVersioning = false; // S3-specific fields - @Column @Size(max = 255, message = "Bucket name must not exceed 255 characters") private String bucketName; - @Column @Size(max = 255, message = "Access key must not exceed 255 characters") private String accessKey; - @Column @Size(max = 255, message = "Secret key must not exceed 255 characters") private String secretKey; // SCP-specific fields - @Column private Integer port; - @Column @Size(max = 255, message = "Username must not exceed 255 characters") private String username; - @Column @Size(max = 50, message = "Authentication method must not exceed 50 characters") private String authenticationMethod; // "SSH_KEY", "PASSWORD" - @Column(columnDefinition = "TEXT") private String sshKey; - @Column @Size(max = 500, message = "Remote path must not exceed 500 characters") private String remotePath; - @Column(columnDefinition = "TEXT") private String additionalInfo; - @Column(nullable = false) @NotBlank(message = "Resource manager is required") @Size(max = 255, message = "Resource manager must not exceed 255 characters") private String resourceManager; // Gateway name or organization - @Override - public ResourceTypeEnum getType() { - return ResourceTypeEnum.STORAGE_RESOURCE; - } + // System fields + private boolean enabled = true; + private Long creationTime; + private Long updateTime; // Default constructor - public StorageResource() {} - - // Constructor for mock data creation - public StorageResource(String name, String description, String hostname, String storageType, - Long capacityTB, String accessProtocol, String endpoint, - Boolean supportsEncryption, Boolean supportsVersioning, - String additionalInfo, String resourceManager) { - this.setName(name); - this.setDescription(description); - this.hostname = hostname; - this.storageType = storageType; - this.capacityTB = capacityTB; - this.accessProtocol = accessProtocol; - this.endpoint = endpoint; - this.supportsEncryption = supportsEncryption; - this.supportsVersioning = supportsVersioning; - this.additionalInfo = additionalInfo; - this.resourceManager = resourceManager; - - // Set inherited v1 Resource fields (required) - this.setPrivacy(PrivacyEnum.PUBLIC); - this.setState(StateEnum.ACTIVE); - this.setStatus(StatusEnum.VERIFIED); - this.setAuthors(new HashSet<>()); - this.setTags(new HashSet<>()); - this.setHeaderImage(""); // Default empty header image + public StorageResourceDTO() {} + + // Constructor for basic creation + public StorageResourceDTO(String hostName, String storageResourceDescription) { + this.hostName = hostName; + this.storageResourceDescription = storageResourceDescription; } // S3-specific constructor - public StorageResource(String name, String description, String storageType, String endpoint, - String bucketName, String accessKey, String secretKey, - String resourceManager) { - this(name, description, endpoint, storageType, 1000L, "S3", endpoint, true, true, null, resourceManager); + public StorageResourceDTO(String hostName, String storageResourceDescription, String storageType, + String endpoint, String bucketName, String accessKey, String secretKey, + String resourceManager) { + this.hostName = hostName; + this.storageResourceDescription = storageResourceDescription; + this.storageType = storageType; + this.endpoint = endpoint; this.bucketName = bucketName; this.accessKey = accessKey; this.secretKey = secretKey; + this.resourceManager = resourceManager; + this.accessProtocol = "S3"; + this.supportsEncryption = true; + this.supportsVersioning = true; } // SCP-specific constructor - public StorageResource(String name, String description, String hostname, String storageType, - Integer port, String username, String authenticationMethod, String sshKey, - String remotePath, String resourceManager) { - this(name, description, hostname, storageType, 100L, "SCP", hostname, false, false, null, resourceManager); + public StorageResourceDTO(String hostName, String storageResourceDescription, String storageType, + Integer port, String username, String authenticationMethod, String sshKey, + String remotePath, String resourceManager) { + this.hostName = hostName; + this.storageResourceDescription = storageResourceDescription; + this.storageType = storageType; this.port = port; this.username = username; this.authenticationMethod = authenticationMethod; this.sshKey = sshKey; this.remotePath = remotePath; + this.resourceManager = resourceManager; + this.accessProtocol = "SCP"; + this.endpoint = hostName; } - // Getters and Setters for StorageResource-specific fields - public String getHostname() { - return hostname; + // Getters and Setters + public String getStorageResourceId() { + return storageResourceId; } - public void setHostname(String hostname) { - this.hostname = hostname; + public void setStorageResourceId(String storageResourceId) { + this.storageResourceId = storageResourceId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getHostName() { + return hostName; + } + + public void setHostName(String hostName) { + this.hostName = hostName; + } + + public String getStorageResourceDescription() { + return storageResourceDescription; + } + + public void setStorageResourceDescription(String storageResourceDescription) { + this.storageResourceDescription = storageResourceDescription; } public String getStorageType() { @@ -221,23 +224,6 @@ public void setSupportsVersioning(Boolean supportsVersioning) { this.supportsVersioning = supportsVersioning; } - public String getAdditionalInfo() { - return additionalInfo; - } - - public void setAdditionalInfo(String additionalInfo) { - this.additionalInfo = additionalInfo; - } - - public String getResourceManager() { - return resourceManager; - } - - public void setResourceManager(String resourceManager) { - this.resourceManager = resourceManager; - } - - // S3-specific getters and setters public String getBucketName() { return bucketName; } @@ -262,7 +248,6 @@ public void setSecretKey(String secretKey) { this.secretKey = secretKey; } - // SCP-specific getters and setters public Integer getPort() { return port; } @@ -302,4 +287,44 @@ public String getRemotePath() { public void setRemotePath(String remotePath) { this.remotePath = remotePath; } + + public String getAdditionalInfo() { + return additionalInfo; + } + + public void setAdditionalInfo(String additionalInfo) { + this.additionalInfo = additionalInfo; + } + + public String getResourceManager() { + return resourceManager; + } + + public void setResourceManager(String resourceManager) { + this.resourceManager = resourceManager; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Long getCreationTime() { + return creationTime; + } + + public void setCreationTime(Long creationTime) { + this.creationTime = creationTime; + } + + public Long getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Long updateTime) { + this.updateTime = updateTime; + } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/LocalComputeResourceEntity.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/LocalComputeResourceEntity.java new file mode 100644 index 00000000000..4584ca077f4 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/LocalComputeResourceEntity.java @@ -0,0 +1,179 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.entity; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * Local entity that mirrors airavata-api ComputeResourceEntity structure + * Used for local development without external airavata-api dependencies + */ +@Entity +@Table(name = "LOCAL_COMPUTE_RESOURCE") +public class LocalComputeResourceEntity implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "RESOURCE_ID") + private String computeResourceId; + + @Column(name = "CREATION_TIME") + private Timestamp creationTime; + + @Column(name = "ENABLED") + private short enabled; + + @Column(name = "HOST_NAME") + private String hostName; + + @Column(name = "MAX_MEMORY_NODE") + private int maxMemoryPerNode; + + @Column(name = "RESOURCE_DESCRIPTION", length = 4000) + private String resourceDescription; + + @Column(name = "UPDATE_TIME") + private Timestamp updateTime; + + @Column(name = "CPUS_PER_NODE") + private Integer cpusPerNode; + + @Column(name = "DEFAULT_NODE_COUNT") + private Integer defaultNodeCount; + + @Column(name = "DEFAULT_CPU_COUNT") + private Integer defaultCPUCount; + + @Column(name = "DEFAULT_WALLTIME") + private Integer defaultWalltime; + + // Default constructor + public LocalComputeResourceEntity() {} + + // Constructor matching airavata-api pattern + public LocalComputeResourceEntity(String computeResourceId, String hostName, String description) { + this.computeResourceId = computeResourceId; + this.hostName = hostName; + this.resourceDescription = description; + this.enabled = 1; + this.creationTime = new Timestamp(System.currentTimeMillis()); + this.updateTime = new Timestamp(System.currentTimeMillis()); + } + + // Getters and setters (matching airavata-api naming) + public String getComputeResourceId() { + return computeResourceId; + } + + public void setComputeResourceId(String computeResourceId) { + this.computeResourceId = computeResourceId; + } + + public Timestamp getCreationTime() { + return creationTime; + } + + public void setCreationTime(Timestamp creationTime) { + this.creationTime = creationTime; + } + + public short getEnabled() { + return enabled; + } + + public void setEnabled(short enabled) { + this.enabled = enabled; + } + + public String getHostName() { + return hostName; + } + + public void setHostName(String hostName) { + this.hostName = hostName; + } + + public int getMaxMemoryPerNode() { + return maxMemoryPerNode; + } + + public void setMaxMemoryPerNode(int maxMemoryPerNode) { + this.maxMemoryPerNode = maxMemoryPerNode; + } + + public String getResourceDescription() { + return resourceDescription; + } + + public void setResourceDescription(String resourceDescription) { + this.resourceDescription = resourceDescription; + } + + public Timestamp getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Timestamp updateTime) { + this.updateTime = updateTime; + } + + public Integer getCpusPerNode() { + return cpusPerNode; + } + + public void setCpusPerNode(Integer cpusPerNode) { + this.cpusPerNode = cpusPerNode; + } + + public Integer getDefaultNodeCount() { + return defaultNodeCount; + } + + public void setDefaultNodeCount(Integer defaultNodeCount) { + this.defaultNodeCount = defaultNodeCount; + } + + public Integer getDefaultCPUCount() { + return defaultCPUCount; + } + + public void setDefaultCPUCount(Integer defaultCPUCount) { + this.defaultCPUCount = defaultCPUCount; + } + + public Integer getDefaultWalltime() { + return defaultWalltime; + } + + public void setDefaultWalltime(Integer defaultWalltime) { + this.defaultWalltime = defaultWalltime; + } + + @Override + public String toString() { + return "LocalComputeResourceEntity{" + + "computeResourceId='" + computeResourceId + '\'' + + ", hostName='" + hostName + '\'' + + ", enabled=" + enabled + + '}'; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/LocalStorageResourceEntity.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/LocalStorageResourceEntity.java new file mode 100644 index 00000000000..b07206e88d2 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/LocalStorageResourceEntity.java @@ -0,0 +1,124 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.entity; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * Local entity that mirrors airavata-api StorageResourceEntity structure + * Used for local development without external airavata-api dependencies + */ +@Entity +@Table(name = "LOCAL_STORAGE_RESOURCE") +public class LocalStorageResourceEntity implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "STORAGE_RESOURCE_ID") + private String storageResourceId; + + @Column(name = "CREATION_TIME") + private Timestamp creationTime; + + @Column(name = "DESCRIPTION", length = 4000) + private String storageResourceDescription; + + @Column(name = "ENABLED") + private boolean enabled; + + @Column(name = "HOST_NAME") + private String hostName; + + @Column(name = "UPDATE_TIME") + private Timestamp updateTime; + + // Default constructor + public LocalStorageResourceEntity() {} + + // Constructor matching airavata-api pattern + public LocalStorageResourceEntity(String storageResourceId, String hostName, String description, boolean enabled) { + this.storageResourceId = storageResourceId; + this.hostName = hostName; + this.storageResourceDescription = description; + this.enabled = enabled; + this.creationTime = new Timestamp(System.currentTimeMillis()); + this.updateTime = new Timestamp(System.currentTimeMillis()); + } + + // Getters and setters (matching airavata-api naming) + public String getStorageResourceId() { + return storageResourceId; + } + + public void setStorageResourceId(String storageResourceId) { + this.storageResourceId = storageResourceId; + } + + public String getStorageResourceDescription() { + return storageResourceDescription; + } + + public void setStorageResourceDescription(String storageResourceDescription) { + this.storageResourceDescription = storageResourceDescription; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getHostName() { + return hostName; + } + + public void setHostName(String hostName) { + this.hostName = hostName; + } + + public Timestamp getCreationTime() { + return creationTime; + } + + public void setCreationTime(Timestamp creationTime) { + this.creationTime = creationTime; + } + + public Timestamp getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Timestamp updateTime) { + this.updateTime = updateTime; + } + + @Override + public String toString() { + return "LocalStorageResourceEntity{" + + "storageResourceId='" + storageResourceId + '\'' + + ", hostName='" + hostName + '\'' + + ", enabled=" + enabled + + '}'; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/ComputeResourceHandler.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/ComputeResourceHandler.java new file mode 100644 index 00000000000..5f0dbd37da4 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/ComputeResourceHandler.java @@ -0,0 +1,269 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.airavata.research.service.handler; + +import org.apache.airavata.model.appcatalog.computeresource.ComputeResourceDescription; +import org.apache.airavata.registry.api.RegistryService; +import org.apache.airavata.registry.api.exception.RegistryServiceException; +import org.apache.airavata.research.service.config.RegistryServiceConfig; +import org.apache.airavata.research.service.dto.ComputeResourceDTO; +import org.apache.airavata.research.service.util.DTOConverter; +import org.apache.thrift.TException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * Handler for Compute Resource operations using Airavata Registry Service + * Integrates with existing airavata-api infrastructure + */ +@Component +public class ComputeResourceHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(ComputeResourceHandler.class); + + @Autowired + private RegistryServiceConfig.RegistryServiceProvider registryServiceProvider; + + @Autowired + private DTOConverter dtoConverter; + + /** + * Get RegistryService with connection validation + */ + private RegistryService.Iface getRegistryService() throws RegistryServiceException { + return registryServiceProvider.getRegistryService(); + } + + /** + * Check if registry service is available + */ + private boolean isRegistryServiceAvailable() { + return registryServiceProvider.isAvailable(); + } + + /** + * Create a new compute resource using registry service + */ + public ComputeResourceDTO createComputeResource(ComputeResourceDTO dto) throws RegistryServiceException, TException { + LOGGER.debug("Creating compute resource: {}", dto.getHostName()); + + try { + // Convert DTO to Thrift model + ComputeResourceDescription thriftModel = dtoConverter.dtoToThrift(dto); + + // Generate ID if not provided + if (thriftModel.getComputeResourceId() == null || thriftModel.getComputeResourceId().isEmpty()) { + thriftModel.setComputeResourceId(generateComputeResourceId()); + } + + // Use existing registry service to save + String resourceId = getRegistryService().registerComputeResource(thriftModel); + LOGGER.info("Successfully created compute resource with ID: {}", resourceId); + + // Retrieve saved entity with generated/updated fields + ComputeResourceDescription savedModel = getRegistryService().getComputeResource(resourceId); + + // Convert back to DTO for frontend + return dtoConverter.thriftToDTO(savedModel); + + } catch (RegistryServiceException e) { + LOGGER.error("Failed to create compute resource: {}", e.getMessage(), e); + throw e; + } catch (TException e) { + LOGGER.error("Thrift error creating compute resource: {}", e.getMessage(), e); + throw e; + } catch (Exception e) { + LOGGER.error("Unexpected error creating compute resource", e); + throw new RegistryServiceException("Failed to create compute resource: " + e.getMessage()); + } + } + + /** + * Get compute resource by ID + */ + public ComputeResourceDTO getComputeResource(String resourceId) throws RegistryServiceException, TException { + LOGGER.debug("Retrieving compute resource: {}", resourceId); + + try { + ComputeResourceDescription thriftModel = getRegistryService().getComputeResource(resourceId); + return dtoConverter.thriftToDTO(thriftModel); + } catch (RegistryServiceException e) { + LOGGER.error("Failed to get compute resource {}: {}", resourceId, e.getMessage()); + throw e; + } catch (TException e) { + LOGGER.error("Thrift error getting compute resource {}: {}", resourceId, e.getMessage()); + throw e; + } + } + + /** + * Get all compute resources + */ + public List getAllComputeResources() throws RegistryServiceException, TException { + LOGGER.debug("Retrieving all compute resources"); + + try { + // Get compute resource names (ID -> Name mapping) + Map computeResourceNames = getRegistryService().getAllComputeResourceNames(); + + // Fetch full details for each compute resource + return computeResourceNames.keySet().stream() + .map(resourceId -> { + try { + return getRegistryService().getComputeResource(resourceId); + } catch (RegistryServiceException e) { + LOGGER.warn("Failed to get compute resource {}: {}", resourceId, e.getMessage()); + return null; + } catch (TException e) { + LOGGER.warn("Thrift error getting compute resource {}: {}", resourceId, e.getMessage()); + return null; + } + }) + .filter(thriftModel -> thriftModel != null) + .map(thriftModel -> dtoConverter.thriftToDTO(thriftModel)) + .collect(Collectors.toList()); + + } catch (RegistryServiceException e) { + LOGGER.error("Failed to get all compute resources: {}", e.getMessage()); + throw e; + } catch (TException e) { + LOGGER.error("Thrift error getting all compute resources: {}", e.getMessage()); + throw e; + } + } + + /** + * Update compute resource + */ + public ComputeResourceDTO updateComputeResource(String resourceId, ComputeResourceDTO dto) throws RegistryServiceException, TException { + LOGGER.debug("Updating compute resource: {}", resourceId); + + try { + // Ensure DTO has the correct ID + dto.setComputeResourceId(resourceId); + + // Convert DTO to Thrift model + ComputeResourceDescription thriftModel = dtoConverter.dtoToThrift(dto); + + // Use existing registry service to update + boolean updated = getRegistryService().updateComputeResource(resourceId, thriftModel); + + if (updated) { + LOGGER.info("Successfully updated compute resource: {}", resourceId); + + // Retrieve updated entity + ComputeResourceDescription updatedModel = getRegistryService().getComputeResource(resourceId); + return dtoConverter.thriftToDTO(updatedModel); + } else { + throw new RegistryServiceException("Failed to update compute resource: " + resourceId); + } + + } catch (RegistryServiceException e) { + LOGGER.error("Failed to update compute resource {}: {}", resourceId, e.getMessage()); + throw e; + } catch (TException e) { + LOGGER.error("Thrift error updating compute resource {}: {}", resourceId, e.getMessage()); + throw e; + } + } + + /** + * Delete compute resource + */ + public void deleteComputeResource(String resourceId) throws RegistryServiceException, TException { + LOGGER.debug("Deleting compute resource: {}", resourceId); + + try { + boolean deleted = getRegistryService().deleteComputeResource(resourceId); + + if (deleted) { + LOGGER.info("Successfully deleted compute resource: {}", resourceId); + } else { + throw new RegistryServiceException("Failed to delete compute resource: " + resourceId); + } + + } catch (RegistryServiceException e) { + LOGGER.error("Failed to delete compute resource {}: {}", resourceId, e.getMessage()); + throw e; + } catch (TException e) { + LOGGER.error("Thrift error deleting compute resource {}: {}", resourceId, e.getMessage()); + throw e; + } + } + + /** + * Search compute resources by keyword + * Note: This is a simplified implementation - Airavata registry might have more sophisticated search + */ + public List searchComputeResources(String keyword) throws RegistryServiceException, TException { + LOGGER.debug("Searching compute resources with keyword: {}", keyword); + + try { + // Get all compute resources and filter by keyword + List allResources = getAllComputeResources(); + + String lowerKeyword = keyword.toLowerCase(); + return allResources.stream() + .filter(resource -> + (resource.getHostName() != null && resource.getHostName().toLowerCase().contains(lowerKeyword)) || + (resource.getResourceDescription() != null && resource.getResourceDescription().toLowerCase().contains(lowerKeyword)) || + (resource.getComputeType() != null && resource.getComputeType().toLowerCase().contains(lowerKeyword)) || + (resource.getOperatingSystem() != null && resource.getOperatingSystem().toLowerCase().contains(lowerKeyword)) + ) + .collect(Collectors.toList()); + + } catch (RegistryServiceException e) { + LOGGER.error("Failed to search compute resources: {}", e.getMessage()); + throw e; + } catch (TException e) { + LOGGER.error("Thrift error searching compute resources: {}", e.getMessage()); + throw e; + } + } + + /** + * Check if compute resource exists + */ + public boolean existsComputeResource(String resourceId) { + try { + ComputeResourceDescription resource = getRegistryService().getComputeResource(resourceId); + return resource != null; + } catch (RegistryServiceException e) { + LOGGER.debug("Compute resource {} does not exist: {}", resourceId, e.getMessage()); + return false; + } catch (TException e) { + LOGGER.debug("Thrift error checking compute resource {}: {}", resourceId, e.getMessage()); + return false; + } + } + + /** + * Generate unique compute resource ID + */ + private String generateComputeResourceId() { + return "compute_" + UUID.randomUUID().toString().replace("-", ""); + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/LocalComputeResourceHandler.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/LocalComputeResourceHandler.java new file mode 100644 index 00000000000..b98c0bfb3a0 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/LocalComputeResourceHandler.java @@ -0,0 +1,230 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.handler; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.sql.Timestamp; +import org.apache.airavata.research.service.entity.LocalComputeResourceEntity; +import org.apache.airavata.research.service.dto.ComputeResourceDTO; +import org.apache.airavata.research.service.repository.LocalComputeResourceRepository; +import org.apache.airavata.research.service.util.DTOConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * Local handler for Compute Resource operations using airavata-api entities with local database + * Alternative to external registry services for development + */ +@Component("localComputeResourceHandler") +public class LocalComputeResourceHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(LocalComputeResourceHandler.class); + + private final LocalComputeResourceRepository computeResourceRepository; + private final DTOConverter dtoConverter; + + public LocalComputeResourceHandler(LocalComputeResourceRepository computeResourceRepository, + DTOConverter dtoConverter) { + this.computeResourceRepository = computeResourceRepository; + this.dtoConverter = dtoConverter; + } + + /** + * Get all enabled compute resources + */ + public List getAllComputeResources() { + LOGGER.info("Getting all local compute resources"); + + try { + List entities = computeResourceRepository.findAllEnabledOrderByCreationTime(); + List dtos = new ArrayList<>(); + + for (LocalComputeResourceEntity entity : entities) { + ComputeResourceDTO dto = dtoConverter.computeEntityToDTO(entity); + dtos.add(dto); + } + + LOGGER.info("Found {} local compute resources", dtos.size()); + return dtos; + } catch (Exception e) { + LOGGER.error("Failed to get local compute resources", e); + throw new RuntimeException("Failed to get local compute resources", e); + } + } + + /** + * Search compute resources by keyword + */ + public List searchComputeResources(String keyword) { + LOGGER.info("Searching local compute resources with keyword: {}", keyword); + + try { + List entities; + + if (keyword == null || keyword.trim().isEmpty()) { + entities = computeResourceRepository.findAllEnabledOrderByCreationTime(); + } else { + entities = computeResourceRepository.searchEnabledComputeResources(keyword.trim()); + } + + List dtos = new ArrayList<>(); + for (LocalComputeResourceEntity entity : entities) { + ComputeResourceDTO dto = dtoConverter.computeEntityToDTO(entity); + dtos.add(dto); + } + + LOGGER.info("Found {} compute resources matching keyword '{}'", dtos.size(), keyword); + return dtos; + } catch (Exception e) { + LOGGER.error("Failed to search local compute resources", e); + throw new RuntimeException("Failed to search local compute resources", e); + } + } + + /** + * Get compute resource by ID + */ + public ComputeResourceDTO getComputeResource(String computeResourceId) { + LOGGER.info("Getting local compute resource by ID: {}", computeResourceId); + + try { + Optional entityOpt = computeResourceRepository.findById(computeResourceId); + + if (entityOpt.isEmpty()) { + LOGGER.warn("Compute resource not found with ID: {}", computeResourceId); + throw new RuntimeException("Compute resource not found with ID: " + computeResourceId); + } + + LocalComputeResourceEntity entity = entityOpt.get(); + ComputeResourceDTO dto = dtoConverter.computeEntityToDTO(entity); + + LOGGER.info("Found local compute resource: {}", entity.getHostName()); + return dto; + } catch (Exception e) { + LOGGER.error("Failed to get local compute resource by ID: {}", computeResourceId, e); + throw new RuntimeException("Failed to get local compute resource", e); + } + } + + /** + * Create new compute resource + */ + public ComputeResourceDTO createComputeResource(ComputeResourceDTO computeResourceDTO) { + LOGGER.info("Creating local compute resource: {}", computeResourceDTO.getHostName()); + + try { + // Convert DTO to entity using existing DTOConverter + LocalComputeResourceEntity entity = dtoConverter.computeResourceDTOToEntity(computeResourceDTO); + + // Set system fields + entity.setComputeResourceId(UUID.randomUUID().toString()); + entity.setEnabled((short) 1); + entity.setCreationTime(new Timestamp(System.currentTimeMillis())); + entity.setUpdateTime(new Timestamp(System.currentTimeMillis())); + + // Save to local database + LocalComputeResourceEntity savedEntity = computeResourceRepository.save(entity); + + // Convert back to DTO + ComputeResourceDTO savedDTO = dtoConverter.computeEntityToDTO(savedEntity); + + LOGGER.info("Created local compute resource with ID: {}", savedEntity.getComputeResourceId()); + return savedDTO; + } catch (Exception e) { + LOGGER.error("Failed to create local compute resource", e); + throw new RuntimeException("Failed to create local compute resource", e); + } + } + + /** + * Update existing compute resource + */ + public ComputeResourceDTO updateComputeResource(String computeResourceId, ComputeResourceDTO computeResourceDTO) { + LOGGER.info("Updating local compute resource: {}", computeResourceId); + + try { + Optional existingOpt = computeResourceRepository.findById(computeResourceId); + + if (existingOpt.isEmpty()) { + throw new RuntimeException("Compute resource not found with ID: " + computeResourceId); + } + + // Convert DTO to entity + LocalComputeResourceEntity updatedEntity = dtoConverter.computeResourceDTOToEntity(computeResourceDTO); + + // Preserve system fields + LocalComputeResourceEntity existing = existingOpt.get(); + updatedEntity.setComputeResourceId(computeResourceId); + updatedEntity.setCreationTime(existing.getCreationTime()); + updatedEntity.setUpdateTime(new Timestamp(System.currentTimeMillis())); + + // Save updated entity + LocalComputeResourceEntity savedEntity = computeResourceRepository.save(updatedEntity); + + // Convert back to DTO + ComputeResourceDTO savedDTO = dtoConverter.computeEntityToDTO(savedEntity); + + LOGGER.info("Updated local compute resource: {}", computeResourceId); + return savedDTO; + } catch (Exception e) { + LOGGER.error("Failed to update local compute resource: {}", computeResourceId, e); + throw new RuntimeException("Failed to update local compute resource", e); + } + } + + /** + * Delete compute resource + */ + public void deleteComputeResource(String computeResourceId) { + LOGGER.info("Deleting local compute resource: {}", computeResourceId); + + try { + if (!computeResourceRepository.existsById(computeResourceId)) { + throw new RuntimeException("Compute resource not found with ID: " + computeResourceId); + } + + computeResourceRepository.deleteById(computeResourceId); + LOGGER.info("Deleted local compute resource: {}", computeResourceId); + } catch (Exception e) { + LOGGER.error("Failed to delete local compute resource: {}", computeResourceId, e); + throw new RuntimeException("Failed to delete local compute resource", e); + } + } + + /** + * Check if compute resource exists + */ + public boolean existsComputeResource(String computeResourceId) { + LOGGER.debug("Checking if compute resource exists: {}", computeResourceId); + + try { + boolean exists = computeResourceRepository.existsById(computeResourceId); + LOGGER.debug("Compute resource {} exists: {}", computeResourceId, exists); + return exists; + } catch (Exception e) { + LOGGER.error("Failed to check compute resource existence: {}", computeResourceId, e); + return false; + } + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/LocalStorageResourceHandler.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/LocalStorageResourceHandler.java new file mode 100644 index 00000000000..6b88ba48b4e --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/LocalStorageResourceHandler.java @@ -0,0 +1,257 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.handler; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.sql.Timestamp; +import org.apache.airavata.research.service.entity.LocalStorageResourceEntity; +import org.apache.airavata.research.service.dto.StorageResourceDTO; +import org.apache.airavata.research.service.repository.LocalStorageResourceRepository; +import org.apache.airavata.research.service.util.DTOConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * Local handler for Storage Resource operations using airavata-api entities with local database + * Alternative to external registry services for development + */ +@Component("localStorageResourceHandler") +public class LocalStorageResourceHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(LocalStorageResourceHandler.class); + + private final LocalStorageResourceRepository storageResourceRepository; + private final DTOConverter dtoConverter; + + public LocalStorageResourceHandler(LocalStorageResourceRepository storageResourceRepository, + DTOConverter dtoConverter) { + this.storageResourceRepository = storageResourceRepository; + this.dtoConverter = dtoConverter; + } + + /** + * Get all enabled storage resources + */ + public List getAllStorageResources() { + LOGGER.info("Getting all local storage resources"); + + try { + List entities = storageResourceRepository.findAllEnabledOrderByCreationTime(); + List dtos = new ArrayList<>(); + + for (LocalStorageResourceEntity entity : entities) { + StorageResourceDTO dto = dtoConverter.storageEntityToDTO(entity); + dtos.add(dto); + } + + LOGGER.info("Found {} local storage resources", dtos.size()); + return dtos; + } catch (Exception e) { + LOGGER.error("Failed to get local storage resources", e); + throw new RuntimeException("Failed to get local storage resources", e); + } + } + + /** + * Search storage resources by keyword + */ + public List searchStorageResources(String keyword) { + LOGGER.info("Searching local storage resources with keyword: {}", keyword); + + try { + List entities; + + if (keyword == null || keyword.trim().isEmpty()) { + entities = storageResourceRepository.findAllEnabledOrderByCreationTime(); + } else { + entities = storageResourceRepository.searchEnabledStorageResources(keyword.trim()); + } + + List dtos = new ArrayList<>(); + for (LocalStorageResourceEntity entity : entities) { + StorageResourceDTO dto = dtoConverter.storageEntityToDTO(entity); + dtos.add(dto); + } + + LOGGER.info("Found {} storage resources matching keyword '{}'", dtos.size(), keyword); + return dtos; + } catch (Exception e) { + LOGGER.error("Failed to search local storage resources", e); + throw new RuntimeException("Failed to search local storage resources", e); + } + } + + /** + * Get storage resource by ID + */ + public StorageResourceDTO getStorageResource(String storageResourceId) { + LOGGER.info("Getting local storage resource by ID: {}", storageResourceId); + + try { + Optional entityOpt = storageResourceRepository.findById(storageResourceId); + + if (entityOpt.isEmpty()) { + LOGGER.warn("Storage resource not found with ID: {}", storageResourceId); + throw new RuntimeException("Storage resource not found with ID: " + storageResourceId); + } + + LocalStorageResourceEntity entity = entityOpt.get(); + StorageResourceDTO dto = dtoConverter.storageEntityToDTO(entity); + + LOGGER.info("Found local storage resource: {}", entity.getHostName()); + return dto; + } catch (Exception e) { + LOGGER.error("Failed to get local storage resource by ID: {}", storageResourceId, e); + throw new RuntimeException("Failed to get local storage resource", e); + } + } + + /** + * Create new storage resource + */ + public StorageResourceDTO createStorageResource(StorageResourceDTO storageResourceDTO) { + LOGGER.info("Creating local storage resource: {}", storageResourceDTO.getHostName()); + + try { + // Convert DTO to entity using existing DTOConverter + LocalStorageResourceEntity entity = dtoConverter.storageResourceDTOToEntity(storageResourceDTO); + + // Set system fields + entity.setStorageResourceId(UUID.randomUUID().toString()); + entity.setEnabled(true); + entity.setCreationTime(new Timestamp(System.currentTimeMillis())); + entity.setUpdateTime(new Timestamp(System.currentTimeMillis())); + + // Save to local database + LocalStorageResourceEntity savedEntity = storageResourceRepository.save(entity); + + // Convert back to DTO + StorageResourceDTO savedDTO = dtoConverter.storageEntityToDTO(savedEntity); + + LOGGER.info("Created local storage resource with ID: {}", savedEntity.getStorageResourceId()); + return savedDTO; + } catch (Exception e) { + LOGGER.error("Failed to create local storage resource", e); + throw new RuntimeException("Failed to create local storage resource", e); + } + } + + /** + * Update existing storage resource + */ + public StorageResourceDTO updateStorageResource(String storageResourceId, StorageResourceDTO storageResourceDTO) { + LOGGER.info("Updating local storage resource: {}", storageResourceId); + + try { + Optional existingOpt = storageResourceRepository.findById(storageResourceId); + + if (existingOpt.isEmpty()) { + throw new RuntimeException("Storage resource not found with ID: " + storageResourceId); + } + + // Convert DTO to entity + LocalStorageResourceEntity updatedEntity = dtoConverter.storageResourceDTOToEntity(storageResourceDTO); + + // Preserve system fields + LocalStorageResourceEntity existing = existingOpt.get(); + updatedEntity.setStorageResourceId(storageResourceId); + updatedEntity.setCreationTime(existing.getCreationTime()); + updatedEntity.setUpdateTime(new Timestamp(System.currentTimeMillis())); + + // Save updated entity + LocalStorageResourceEntity savedEntity = storageResourceRepository.save(updatedEntity); + + // Convert back to DTO + StorageResourceDTO savedDTO = dtoConverter.storageEntityToDTO(savedEntity); + + LOGGER.info("Updated local storage resource: {}", storageResourceId); + return savedDTO; + } catch (Exception e) { + LOGGER.error("Failed to update local storage resource: {}", storageResourceId, e); + throw new RuntimeException("Failed to update local storage resource", e); + } + } + + /** + * Delete storage resource + */ + public void deleteStorageResource(String storageResourceId) { + LOGGER.info("Deleting local storage resource: {}", storageResourceId); + + try { + if (!storageResourceRepository.existsById(storageResourceId)) { + throw new RuntimeException("Storage resource not found with ID: " + storageResourceId); + } + + storageResourceRepository.deleteById(storageResourceId); + LOGGER.info("Deleted local storage resource: {}", storageResourceId); + } catch (Exception e) { + LOGGER.error("Failed to delete local storage resource: {}", storageResourceId, e); + throw new RuntimeException("Failed to delete local storage resource", e); + } + } + + /** + * Get storage resources by storage type + */ + public List getStorageResourcesByType(String storageType) { + LOGGER.info("Getting local storage resources by type: {}", storageType); + + try { + List entities = storageResourceRepository.findAllEnabledOrderByCreationTime(); + List dtos = new ArrayList<>(); + + for (LocalStorageResourceEntity entity : entities) { + StorageResourceDTO dto = dtoConverter.storageEntityToDTO(entity); + // Filter by storage type from UI fields + if (storageType == null || storageType.isEmpty() || + (dto.getStorageType() != null && dto.getStorageType().equalsIgnoreCase(storageType))) { + dtos.add(dto); + } + } + + LOGGER.info("Found {} storage resources of type '{}'", dtos.size(), storageType); + return dtos; + } catch (Exception e) { + LOGGER.error("Failed to get storage resources by type", e); + throw new RuntimeException("Failed to get storage resources by type", e); + } + } + + /** + * Check if storage resource exists + */ + public boolean existsStorageResource(String storageResourceId) { + LOGGER.debug("Checking if storage resource exists: {}", storageResourceId); + + try { + boolean exists = storageResourceRepository.existsById(storageResourceId); + LOGGER.debug("Storage resource {} exists: {}", storageResourceId, exists); + return exists; + } catch (Exception e) { + LOGGER.error("Failed to check storage resource existence: {}", storageResourceId, e); + return false; + } + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/StorageResourceHandler.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/StorageResourceHandler.java new file mode 100644 index 00000000000..3e03620fc26 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/StorageResourceHandler.java @@ -0,0 +1,300 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.airavata.research.service.handler; + +import org.apache.airavata.model.appcatalog.storageresource.StorageResourceDescription; +import org.apache.airavata.registry.api.RegistryService; +import org.apache.airavata.registry.api.exception.RegistryServiceException; +import org.apache.airavata.research.service.config.RegistryServiceConfig; +import org.apache.airavata.research.service.dto.StorageResourceDTO; +import org.apache.airavata.research.service.util.DTOConverter; +import org.apache.thrift.TException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * Handler for Storage Resource operations using Airavata Registry Service + * Integrates with existing airavata-api infrastructure + */ +@Component +public class StorageResourceHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(StorageResourceHandler.class); + + @Autowired + private RegistryServiceConfig.RegistryServiceProvider registryServiceProvider; + + @Autowired + private DTOConverter dtoConverter; + + /** + * Get RegistryService with connection validation + */ + private RegistryService.Iface getRegistryService() throws RegistryServiceException { + return registryServiceProvider.getRegistryService(); + } + + /** + * Check if registry service is available + */ + private boolean isRegistryServiceAvailable() { + return registryServiceProvider.isAvailable(); + } + + /** + * Create a new storage resource using registry service + */ + public StorageResourceDTO createStorageResource(StorageResourceDTO dto) throws RegistryServiceException, TException { + LOGGER.debug("Creating storage resource: {}", dto.getHostName()); + + try { + // Convert DTO to Thrift model + StorageResourceDescription thriftModel = dtoConverter.dtoToThrift(dto); + + // Generate ID if not provided + if (thriftModel.getStorageResourceId() == null || thriftModel.getStorageResourceId().isEmpty()) { + thriftModel.setStorageResourceId(generateStorageResourceId()); + } + + // Set timestamps + long currentTime = System.currentTimeMillis(); + thriftModel.setCreationTime(currentTime); + thriftModel.setUpdateTime(currentTime); + + // Use existing registry service to save + String resourceId = getRegistryService().registerStorageResource(thriftModel); + LOGGER.info("Successfully created storage resource with ID: {}", resourceId); + + // Retrieve saved entity with generated/updated fields + StorageResourceDescription savedModel = getRegistryService().getStorageResource(resourceId); + + // Convert back to DTO for frontend + return dtoConverter.thriftToDTO(savedModel); + + } catch (RegistryServiceException e) { + LOGGER.error("Failed to create storage resource: {}", e.getMessage(), e); + throw e; + } catch (TException e) { + LOGGER.error("Thrift error creating storage resource: {}", e.getMessage(), e); + throw e; + } catch (Exception e) { + LOGGER.error("Unexpected error creating storage resource", e); + throw new RegistryServiceException("Failed to create storage resource: " + e.getMessage()); + } + } + + /** + * Get storage resource by ID + */ + public StorageResourceDTO getStorageResource(String resourceId) throws RegistryServiceException, TException { + LOGGER.debug("Retrieving storage resource: {}", resourceId); + + try { + StorageResourceDescription thriftModel = getRegistryService().getStorageResource(resourceId); + return dtoConverter.thriftToDTO(thriftModel); + } catch (RegistryServiceException e) { + LOGGER.error("Failed to get storage resource {}: {}", resourceId, e.getMessage()); + throw e; + } catch (TException e) { + LOGGER.error("Thrift error getting storage resource {}: {}", resourceId, e.getMessage()); + throw e; + } + } + + /** + * Get all storage resources + */ + public List getAllStorageResources() throws RegistryServiceException, TException { + LOGGER.debug("Retrieving all storage resources"); + + try { + // Get storage resource names (ID -> Name mapping) + Map storageResourceNames = getRegistryService().getAllStorageResourceNames(); + + // Fetch full details for each storage resource + return storageResourceNames.keySet().stream() + .map(resourceId -> { + try { + return getRegistryService().getStorageResource(resourceId); + } catch (RegistryServiceException e) { + LOGGER.warn("Failed to get storage resource {}: {}", resourceId, e.getMessage()); + return null; + } catch (TException e) { + LOGGER.warn("Thrift error getting storage resource {}: {}", resourceId, e.getMessage()); + return null; + } + }) + .filter(thriftModel -> thriftModel != null) + .map(thriftModel -> dtoConverter.thriftToDTO(thriftModel)) + .collect(Collectors.toList()); + + } catch (RegistryServiceException e) { + LOGGER.error("Failed to get all storage resources: {}", e.getMessage()); + throw e; + } catch (TException e) { + LOGGER.error("Thrift error getting all storage resources: {}", e.getMessage()); + throw e; + } + } + + /** + * Update storage resource + */ + public StorageResourceDTO updateStorageResource(String resourceId, StorageResourceDTO dto) throws RegistryServiceException, TException { + LOGGER.debug("Updating storage resource: {}", resourceId); + + try { + // Ensure DTO has the correct ID + dto.setStorageResourceId(resourceId); + + // Convert DTO to Thrift model + StorageResourceDescription thriftModel = dtoConverter.dtoToThrift(dto); + + // Set update timestamp + thriftModel.setUpdateTime(System.currentTimeMillis()); + + // Use existing registry service to update + boolean updated = getRegistryService().updateStorageResource(resourceId, thriftModel); + + if (updated) { + LOGGER.info("Successfully updated storage resource: {}", resourceId); + + // Retrieve updated entity + StorageResourceDescription updatedModel = getRegistryService().getStorageResource(resourceId); + return dtoConverter.thriftToDTO(updatedModel); + } else { + throw new RegistryServiceException("Failed to update storage resource: " + resourceId); + } + + } catch (RegistryServiceException e) { + LOGGER.error("Failed to update storage resource {}: {}", resourceId, e.getMessage()); + throw e; + } catch (TException e) { + LOGGER.error("Thrift error updating storage resource {}: {}", resourceId, e.getMessage()); + throw e; + } + } + + /** + * Delete storage resource + */ + public void deleteStorageResource(String resourceId) throws RegistryServiceException, TException { + LOGGER.debug("Deleting storage resource: {}", resourceId); + + try { + boolean deleted = getRegistryService().deleteStorageResource(resourceId); + + if (deleted) { + LOGGER.info("Successfully deleted storage resource: {}", resourceId); + } else { + throw new RegistryServiceException("Failed to delete storage resource: " + resourceId); + } + + } catch (RegistryServiceException e) { + LOGGER.error("Failed to delete storage resource {}: {}", resourceId, e.getMessage()); + throw e; + } catch (TException e) { + LOGGER.error("Thrift error deleting storage resource {}: {}", resourceId, e.getMessage()); + throw e; + } + } + + /** + * Search storage resources by keyword + * Note: This is a simplified implementation - Airavata registry might have more sophisticated search + */ + public List searchStorageResources(String keyword) throws RegistryServiceException, TException { + LOGGER.debug("Searching storage resources with keyword: {}", keyword); + + try { + // Get all storage resources and filter by keyword + List allResources = getAllStorageResources(); + + String lowerKeyword = keyword.toLowerCase(); + return allResources.stream() + .filter(resource -> + (resource.getHostName() != null && resource.getHostName().toLowerCase().contains(lowerKeyword)) || + (resource.getStorageResourceDescription() != null && resource.getStorageResourceDescription().toLowerCase().contains(lowerKeyword)) || + (resource.getStorageType() != null && resource.getStorageType().toLowerCase().contains(lowerKeyword)) || + (resource.getAccessProtocol() != null && resource.getAccessProtocol().toLowerCase().contains(lowerKeyword)) + ) + .collect(Collectors.toList()); + + } catch (RegistryServiceException e) { + LOGGER.error("Failed to search storage resources: {}", e.getMessage()); + throw e; + } catch (TException e) { + LOGGER.error("Thrift error searching storage resources: {}", e.getMessage()); + throw e; + } + } + + /** + * Filter storage resources by type + */ + public List getStorageResourcesByType(String storageType) throws RegistryServiceException, TException { + LOGGER.debug("Filtering storage resources by type: {}", storageType); + + try { + List allResources = getAllStorageResources(); + + return allResources.stream() + .filter(resource -> resource.getStorageType() != null && + resource.getStorageType().equalsIgnoreCase(storageType)) + .collect(Collectors.toList()); + + } catch (RegistryServiceException e) { + LOGGER.error("Failed to filter storage resources by type: {}", e.getMessage()); + throw e; + } catch (TException e) { + LOGGER.error("Thrift error filtering storage resources by type: {}", e.getMessage()); + throw e; + } + } + + /** + * Check if storage resource exists + */ + public boolean existsStorageResource(String resourceId) { + try { + StorageResourceDescription resource = getRegistryService().getStorageResource(resourceId); + return resource != null; + } catch (RegistryServiceException e) { + LOGGER.debug("Storage resource {} does not exist: {}", resourceId, e.getMessage()); + return false; + } catch (TException e) { + LOGGER.debug("Thrift error checking storage resource {}: {}", resourceId, e.getMessage()); + return false; + } + } + + /** + * Generate unique storage resource ID + */ + private String generateStorageResourceId() { + return "storage_" + UUID.randomUUID().toString().replace("-", ""); + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/StorageResource.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/StorageResource.java new file mode 100644 index 00000000000..ab37177060d --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/StorageResource.java @@ -0,0 +1,60 @@ +package org.apache.airavata.research.service.model.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "storage_resources") +public class StorageResource { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String storage; + + @Column(name = "storage_type", nullable = false) + private String storageType; + + @Column(nullable = false) + private String status; + + private String description; + + // Constructors + public StorageResource() {} + + public StorageResource(String name, String storage, String storageType, String status, String description) { + this.name = name; + this.storage = storage; + this.storageType = storageType; + this.status = status; + this.description = description; + } + + // Getters and setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getStorage() { return storage; } + public void setStorage(String storage) { this.storage = storage; } + + public String getStorageType() { return storageType; } + public void setStorageType(String storageType) { this.storageType = storageType; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/StorageResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/StorageResourceRepository.java new file mode 100644 index 00000000000..0519ecba6ea --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/StorageResourceRepository.java @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/LocalComputeResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/LocalComputeResourceRepository.java new file mode 100644 index 00000000000..3b1b31edf67 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/LocalComputeResourceRepository.java @@ -0,0 +1,55 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.repository; + +import java.util.List; +import org.apache.airavata.research.service.entity.LocalComputeResourceEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +/** + * Local JPA Repository for airavata-api LocalComputeResourceEntity + * Used for local development data instead of external registry services + */ +@Repository +public interface LocalComputeResourceRepository extends JpaRepository { + + // Find enabled compute resources + List findByEnabled(short enabled); + + // Search by hostname + List findByHostNameContainingIgnoreCase(String hostName); + + // Search by description + List findByResourceDescriptionContainingIgnoreCase(String description); + + // Combined search functionality + @Query("SELECT c FROM LocalComputeResourceEntity c WHERE " + + "c.enabled = 1 AND (" + + "LOWER(c.hostName) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + + "LOWER(c.resourceDescription) LIKE LOWER(CONCAT('%', :keyword, '%')))") + List searchEnabledComputeResources(@Param("keyword") String keyword); + + // Get all enabled compute resources (for public API) + @Query("SELECT c FROM LocalComputeResourceEntity c WHERE c.enabled = 1 ORDER BY c.creationTime DESC") + List findAllEnabledOrderByCreationTime(); +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/LocalStorageResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/LocalStorageResourceRepository.java new file mode 100644 index 00000000000..745c28fae4c --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/LocalStorageResourceRepository.java @@ -0,0 +1,55 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.repository; + +import java.util.List; +import org.apache.airavata.research.service.entity.LocalStorageResourceEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +/** + * Local JPA Repository for airavata-api StorageResourceEntity + * Used for local development data instead of external registry services + */ +@Repository +public interface LocalStorageResourceRepository extends JpaRepository { + + // Find enabled storage resources + List findByEnabled(boolean enabled); + + // Search by hostname + List findByHostNameContainingIgnoreCase(String hostName); + + // Search by description + List findByStorageResourceDescriptionContainingIgnoreCase(String description); + + // Combined search functionality + @Query("SELECT s FROM LocalStorageResourceEntity s WHERE " + + "s.enabled = true AND (" + + "LOWER(s.hostName) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + + "LOWER(s.storageResourceDescription) LIKE LOWER(CONCAT('%', :keyword, '%')))") + List searchEnabledStorageResources(@Param("keyword") String keyword); + + // Get all enabled storage resources (for public API) + @Query("SELECT s FROM LocalStorageResourceEntity s WHERE s.enabled = true ORDER BY s.creationTime DESC") + List findAllEnabledOrderByCreationTime(); +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java new file mode 100644 index 00000000000..ee190026ab3 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java @@ -0,0 +1,781 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.airavata.research.service.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.airavata.model.appcatalog.computeresource.ComputeResourceDescription; +import org.apache.airavata.model.appcatalog.computeresource.BatchQueue; +import org.apache.airavata.model.appcatalog.storageresource.StorageResourceDescription; +import org.apache.airavata.research.service.entity.LocalComputeResourceEntity; +import org.apache.airavata.research.service.entity.LocalStorageResourceEntity; +import org.apache.airavata.research.service.dto.ComputeResourceDTO; +import org.apache.airavata.research.service.dto.ComputeResourceQueueDTO; +import org.apache.airavata.research.service.dto.StorageResourceDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Utility class for converting between Airavata Thrift models and UI DTOs + * Handles JSON serialization of UI-specific fields into description fields + */ +@Component +public class DTOConverter { + + private static final Logger LOGGER = LoggerFactory.getLogger(DTOConverter.class); + private final ObjectMapper objectMapper = new ObjectMapper(); + + // JSON field names for UI-specific data + private static final String UI_FIELDS_KEY = "uiFields"; + private static final String COMPUTE_TYPE_KEY = "computeType"; + private static final String OPERATING_SYSTEM_KEY = "operatingSystem"; + private static final String QUEUE_SYSTEM_KEY = "queueSystem"; + private static final String ADDITIONAL_INFO_KEY = "additionalInfo"; + private static final String RESOURCE_MANAGER_KEY = "resourceManager"; + private static final String SSH_CONFIG_KEY = "sshConfig"; + private static final String SSH_USERNAME_KEY = "sshUsername"; + private static final String SSH_PORT_KEY = "sshPort"; + private static final String AUTH_METHOD_KEY = "authenticationMethod"; + private static final String SSH_KEY_KEY = "sshKey"; + private static final String WORKING_DIR_KEY = "workingDirectory"; + private static final String SCHEDULER_TYPE_KEY = "schedulerType"; + private static final String DATA_MOVEMENT_PROTOCOL_KEY = "dataMovementProtocol"; + + // Storage-specific field names + private static final String STORAGE_TYPE_KEY = "storageType"; + private static final String CAPACITY_TB_KEY = "capacityTB"; + private static final String ACCESS_PROTOCOL_KEY = "accessProtocol"; + private static final String ENDPOINT_KEY = "endpoint"; + private static final String SUPPORTS_ENCRYPTION_KEY = "supportsEncryption"; + private static final String SUPPORTS_VERSIONING_KEY = "supportsVersioning"; + private static final String S3_CONFIG_KEY = "s3Config"; + private static final String BUCKET_NAME_KEY = "bucketName"; + private static final String ACCESS_KEY_KEY = "accessKey"; + private static final String SECRET_KEY_KEY = "secretKey"; + private static final String SCP_CONFIG_KEY = "scpConfig"; + private static final String PORT_KEY = "port"; + private static final String USERNAME_KEY = "username"; + private static final String REMOTE_PATH_KEY = "remotePath"; + + /** + * Convert ComputeResourceDescription to ComputeResourceDTO + */ + public ComputeResourceDTO thriftToDTO(ComputeResourceDescription thriftModel) { + if (thriftModel == null) { + return null; + } + + ComputeResourceDTO dto = new ComputeResourceDTO(); + + // Direct mappings + dto.setComputeResourceId(thriftModel.getComputeResourceId()); + dto.setHostName(thriftModel.getHostName()); + dto.setHostAliases(thriftModel.getHostAliases()); + dto.setIpAddresses(thriftModel.getIpAddresses()); + dto.setEnabled(thriftModel.isEnabled()); + + // Map memory (convert from MB to GB if needed) + if (thriftModel.isSetMaxMemoryPerNode()) { + dto.setMemoryGB(thriftModel.getMaxMemoryPerNode() / 1024); // Assuming thrift is in MB + } + + // Map CPU cores + if (thriftModel.isSetCpusPerNode()) { + dto.setCpuCores(thriftModel.getCpusPerNode()); + } + + // Extract UI-specific fields from resourceDescription JSON + parseResourceDescriptionForComputeResource(thriftModel.getResourceDescription(), dto); + + // Convert batch queues to queue DTOs + if (thriftModel.getBatchQueues() != null) { + dto.setQueues(thriftModel.getBatchQueues().stream() + .map(this::batchQueueToDTO) + .collect(Collectors.toList())); + } + + return dto; + } + + /** + * Convert ComputeResourceDTO to ComputeResourceDescription + */ + public ComputeResourceDescription dtoToThrift(ComputeResourceDTO dto) { + if (dto == null) { + return null; + } + + ComputeResourceDescription thriftModel = new ComputeResourceDescription(); + + // Direct mappings + thriftModel.setComputeResourceId(dto.getComputeResourceId()); + thriftModel.setHostName(dto.getHostName()); + thriftModel.setHostAliases(dto.getHostAliases()); + thriftModel.setIpAddresses(dto.getIpAddresses()); + thriftModel.setEnabled(dto.isEnabled()); + + // Map memory (convert from GB to MB) + if (dto.getMemoryGB() != null) { + thriftModel.setMaxMemoryPerNode(dto.getMemoryGB() * 1024); + } + + // Map CPU cores + if (dto.getCpuCores() != null) { + thriftModel.setCpusPerNode(dto.getCpuCores()); + } + + // Store UI-specific fields as JSON in resourceDescription + thriftModel.setResourceDescription(buildResourceDescriptionForComputeResource(dto)); + + // Convert queue DTOs to batch queues + if (dto.getQueues() != null) { + thriftModel.setBatchQueues(dto.getQueues().stream() + .map(this::dtoToBatchQueue) + .collect(Collectors.toList())); + } + + return thriftModel; + } + + /** + * Convert StorageResourceDescription to StorageResourceDTO + */ + public StorageResourceDTO thriftToDTO(StorageResourceDescription thriftModel) { + if (thriftModel == null) { + return null; + } + + StorageResourceDTO dto = new StorageResourceDTO(); + + // Direct mappings + dto.setStorageResourceId(thriftModel.getStorageResourceId()); + dto.setHostName(thriftModel.getHostName()); + dto.setEnabled(thriftModel.isEnabled()); + dto.setCreationTime(thriftModel.getCreationTime()); + dto.setUpdateTime(thriftModel.getUpdateTime()); + + // Extract UI-specific fields from storageResourceDescription JSON + parseResourceDescriptionForStorageResource(thriftModel.getStorageResourceDescription(), dto); + + return dto; + } + + /** + * Convert StorageResourceDTO to StorageResourceDescription + */ + public StorageResourceDescription dtoToThrift(StorageResourceDTO dto) { + if (dto == null) { + return null; + } + + StorageResourceDescription thriftModel = new StorageResourceDescription(); + + // Direct mappings + thriftModel.setStorageResourceId(dto.getStorageResourceId()); + thriftModel.setHostName(dto.getHostName()); + thriftModel.setEnabled(dto.isEnabled()); + + // Store UI-specific fields as JSON in storageResourceDescription + thriftModel.setStorageResourceDescription(buildResourceDescriptionForStorageResource(dto)); + + return thriftModel; + } + + /** + * Convert BatchQueue to ComputeResourceQueueDTO + */ + public ComputeResourceQueueDTO batchQueueToDTO(BatchQueue batchQueue) { + if (batchQueue == null) { + return null; + } + + ComputeResourceQueueDTO dto = new ComputeResourceQueueDTO(); + dto.setQueueName(batchQueue.getQueueName()); + dto.setQueueDescription(batchQueue.getQueueDescription()); + dto.setMaxRunTime(batchQueue.getMaxRunTime()); + dto.setMaxNodes(batchQueue.getMaxNodes()); + dto.setMaxProcessors(batchQueue.getMaxProcessors()); + dto.setMaxJobsInQueue(batchQueue.getMaxJobsInQueue()); + dto.setMaxMemory(batchQueue.getMaxMemory()); + dto.setCpusPerNode(batchQueue.getCpuPerNode()); + dto.setDefaultNodeCount(batchQueue.getDefaultNodeCount()); + dto.setDefaultCpuCount(batchQueue.getDefaultCPUCount()); + dto.setDefaultWallTime(batchQueue.getDefaultWalltime()); + dto.setQueueSpecificMacros(batchQueue.getQueueSpecificMacros()); + dto.setIsDefaultQueue(batchQueue.isIsDefaultQueue()); + + return dto; + } + + /** + * Convert ComputeResourceQueueDTO to BatchQueue + */ + public BatchQueue dtoToBatchQueue(ComputeResourceQueueDTO dto) { + if (dto == null) { + return null; + } + + BatchQueue batchQueue = new BatchQueue(); + batchQueue.setQueueName(dto.getQueueName()); + batchQueue.setQueueDescription(dto.getQueueDescription()); + batchQueue.setMaxRunTime(dto.getMaxRunTime() != null ? dto.getMaxRunTime() : 0); + batchQueue.setMaxNodes(dto.getMaxNodes() != null ? dto.getMaxNodes() : 0); + batchQueue.setMaxProcessors(dto.getMaxProcessors() != null ? dto.getMaxProcessors() : 0); + batchQueue.setMaxJobsInQueue(dto.getMaxJobsInQueue() != null ? dto.getMaxJobsInQueue() : 0); + batchQueue.setMaxMemory(dto.getMaxMemory() != null ? dto.getMaxMemory() : 0); + batchQueue.setCpuPerNode(dto.getCpusPerNode() != null ? dto.getCpusPerNode() : 0); + batchQueue.setDefaultNodeCount(dto.getDefaultNodeCount() != null ? dto.getDefaultNodeCount() : 0); + batchQueue.setDefaultCPUCount(dto.getDefaultCpuCount() != null ? dto.getDefaultCpuCount() : 0); + batchQueue.setDefaultWalltime(dto.getDefaultWallTime() != null ? dto.getDefaultWallTime() : 0); + batchQueue.setQueueSpecificMacros(dto.getQueueSpecificMacros()); + batchQueue.setIsDefaultQueue(dto.getIsDefaultQueue() != null ? dto.getIsDefaultQueue() : false); + + return batchQueue; + } + + /** + * Parse JSON from resourceDescription and populate ComputeResourceDTO UI fields + */ + private void parseResourceDescriptionForComputeResource(String resourceDescription, ComputeResourceDTO dto) { + if (resourceDescription == null || resourceDescription.trim().isEmpty()) { + return; + } + + try { + JsonNode rootNode = objectMapper.readTree(resourceDescription); + JsonNode uiFieldsNode = rootNode.get(UI_FIELDS_KEY); + + if (uiFieldsNode != null) { + // Extract UI-specific fields + dto.setComputeType(getStringValue(uiFieldsNode, COMPUTE_TYPE_KEY)); + dto.setOperatingSystem(getStringValue(uiFieldsNode, OPERATING_SYSTEM_KEY)); + dto.setQueueSystem(getStringValue(uiFieldsNode, QUEUE_SYSTEM_KEY)); + dto.setAdditionalInfo(getStringValue(uiFieldsNode, ADDITIONAL_INFO_KEY)); + dto.setResourceManager(getStringValue(uiFieldsNode, RESOURCE_MANAGER_KEY)); + dto.setWorkingDirectory(getStringValue(uiFieldsNode, WORKING_DIR_KEY)); + dto.setSchedulerType(getStringValue(uiFieldsNode, SCHEDULER_TYPE_KEY)); + dto.setDataMovementProtocol(getStringValue(uiFieldsNode, DATA_MOVEMENT_PROTOCOL_KEY)); + + // Extract SSH configuration + JsonNode sshConfigNode = uiFieldsNode.get(SSH_CONFIG_KEY); + if (sshConfigNode != null) { + dto.setSshUsername(getStringValue(sshConfigNode, SSH_USERNAME_KEY)); + dto.setSshPort(getIntegerValue(sshConfigNode, SSH_PORT_KEY)); + dto.setAuthenticationMethod(getStringValue(sshConfigNode, AUTH_METHOD_KEY)); + dto.setSshKey(getStringValue(sshConfigNode, SSH_KEY_KEY)); + } + } + + // Set basic description (without UI fields) + JsonNode basicDescNode = rootNode.get("description"); + if (basicDescNode != null) { + dto.setResourceDescription(basicDescNode.asText()); + } + + } catch (JsonProcessingException e) { + LOGGER.warn("Failed to parse resourceDescription JSON, treating as plain text: {}", e.getMessage()); + dto.setResourceDescription(resourceDescription); + } + } + + /** + * Build JSON resourceDescription from ComputeResourceDTO + */ + private String buildResourceDescriptionForComputeResource(ComputeResourceDTO dto) { + Map rootMap = new HashMap<>(); + + // Basic description + if (dto.getResourceDescription() != null) { + rootMap.put("description", dto.getResourceDescription()); + } + + // UI-specific fields + Map uiFields = new HashMap<>(); + uiFields.put(COMPUTE_TYPE_KEY, dto.getComputeType()); + uiFields.put(OPERATING_SYSTEM_KEY, dto.getOperatingSystem()); + uiFields.put(QUEUE_SYSTEM_KEY, dto.getQueueSystem()); + uiFields.put(ADDITIONAL_INFO_KEY, dto.getAdditionalInfo()); + uiFields.put(RESOURCE_MANAGER_KEY, dto.getResourceManager()); + uiFields.put(WORKING_DIR_KEY, dto.getWorkingDirectory()); + uiFields.put(SCHEDULER_TYPE_KEY, dto.getSchedulerType()); + uiFields.put(DATA_MOVEMENT_PROTOCOL_KEY, dto.getDataMovementProtocol()); + + // SSH configuration + Map sshConfig = new HashMap<>(); + sshConfig.put(SSH_USERNAME_KEY, dto.getSshUsername()); + sshConfig.put(SSH_PORT_KEY, dto.getSshPort()); + sshConfig.put(AUTH_METHOD_KEY, dto.getAuthenticationMethod()); + sshConfig.put(SSH_KEY_KEY, dto.getSshKey()); + uiFields.put(SSH_CONFIG_KEY, sshConfig); + + rootMap.put(UI_FIELDS_KEY, uiFields); + + try { + return objectMapper.writeValueAsString(rootMap); + } catch (JsonProcessingException e) { + LOGGER.error("Failed to serialize UI fields to JSON", e); + return dto.getResourceDescription() != null ? dto.getResourceDescription() : ""; + } + } + + /** + * Parse JSON from storageResourceDescription and populate StorageResourceDTO UI fields + */ + private void parseResourceDescriptionForStorageResource(String storageResourceDescription, StorageResourceDTO dto) { + if (storageResourceDescription == null || storageResourceDescription.trim().isEmpty()) { + return; + } + + try { + JsonNode rootNode = objectMapper.readTree(storageResourceDescription); + JsonNode uiFieldsNode = rootNode.get(UI_FIELDS_KEY); + + if (uiFieldsNode != null) { + // Extract UI-specific fields + dto.setStorageType(getStringValue(uiFieldsNode, STORAGE_TYPE_KEY)); + dto.setCapacityTB(getLongValue(uiFieldsNode, CAPACITY_TB_KEY)); + dto.setAccessProtocol(getStringValue(uiFieldsNode, ACCESS_PROTOCOL_KEY)); + dto.setEndpoint(getStringValue(uiFieldsNode, ENDPOINT_KEY)); + dto.setSupportsEncryption(getBooleanValue(uiFieldsNode, SUPPORTS_ENCRYPTION_KEY)); + dto.setSupportsVersioning(getBooleanValue(uiFieldsNode, SUPPORTS_VERSIONING_KEY)); + dto.setAdditionalInfo(getStringValue(uiFieldsNode, ADDITIONAL_INFO_KEY)); + dto.setResourceManager(getStringValue(uiFieldsNode, RESOURCE_MANAGER_KEY)); + + // Extract S3 configuration + JsonNode s3ConfigNode = uiFieldsNode.get(S3_CONFIG_KEY); + if (s3ConfigNode != null) { + dto.setBucketName(getStringValue(s3ConfigNode, BUCKET_NAME_KEY)); + dto.setAccessKey(getStringValue(s3ConfigNode, ACCESS_KEY_KEY)); + dto.setSecretKey(getStringValue(s3ConfigNode, SECRET_KEY_KEY)); + } + + // Extract SCP configuration + JsonNode scpConfigNode = uiFieldsNode.get(SCP_CONFIG_KEY); + if (scpConfigNode != null) { + dto.setPort(getIntegerValue(scpConfigNode, PORT_KEY)); + dto.setUsername(getStringValue(scpConfigNode, USERNAME_KEY)); + dto.setAuthenticationMethod(getStringValue(scpConfigNode, AUTH_METHOD_KEY)); + dto.setSshKey(getStringValue(scpConfigNode, SSH_KEY_KEY)); + dto.setRemotePath(getStringValue(scpConfigNode, REMOTE_PATH_KEY)); + } + } + + // Set basic description (without UI fields) + JsonNode basicDescNode = rootNode.get("description"); + if (basicDescNode != null) { + dto.setStorageResourceDescription(basicDescNode.asText()); + } + + } catch (JsonProcessingException e) { + LOGGER.warn("Failed to parse storageResourceDescription JSON, treating as plain text: {}", e.getMessage()); + dto.setStorageResourceDescription(storageResourceDescription); + } + } + + /** + * Build JSON storageResourceDescription from StorageResourceDTO + */ + private String buildResourceDescriptionForStorageResource(StorageResourceDTO dto) { + Map rootMap = new HashMap<>(); + + // Basic description + if (dto.getStorageResourceDescription() != null) { + rootMap.put("description", dto.getStorageResourceDescription()); + } + + // UI-specific fields + Map uiFields = new HashMap<>(); + uiFields.put(STORAGE_TYPE_KEY, dto.getStorageType()); + uiFields.put(CAPACITY_TB_KEY, dto.getCapacityTB()); + uiFields.put(ACCESS_PROTOCOL_KEY, dto.getAccessProtocol()); + uiFields.put(ENDPOINT_KEY, dto.getEndpoint()); + uiFields.put(SUPPORTS_ENCRYPTION_KEY, dto.getSupportsEncryption()); + uiFields.put(SUPPORTS_VERSIONING_KEY, dto.getSupportsVersioning()); + uiFields.put(ADDITIONAL_INFO_KEY, dto.getAdditionalInfo()); + uiFields.put(RESOURCE_MANAGER_KEY, dto.getResourceManager()); + + // S3 configuration + if ("S3".equalsIgnoreCase(dto.getStorageType())) { + Map s3Config = new HashMap<>(); + s3Config.put(BUCKET_NAME_KEY, dto.getBucketName()); + s3Config.put(ACCESS_KEY_KEY, dto.getAccessKey()); + s3Config.put(SECRET_KEY_KEY, dto.getSecretKey()); + uiFields.put(S3_CONFIG_KEY, s3Config); + } + + // SCP configuration + if ("SCP".equalsIgnoreCase(dto.getStorageType())) { + Map scpConfig = new HashMap<>(); + scpConfig.put(PORT_KEY, dto.getPort()); + scpConfig.put(USERNAME_KEY, dto.getUsername()); + scpConfig.put(AUTH_METHOD_KEY, dto.getAuthenticationMethod()); + scpConfig.put(SSH_KEY_KEY, dto.getSshKey()); + scpConfig.put(REMOTE_PATH_KEY, dto.getRemotePath()); + uiFields.put(SCP_CONFIG_KEY, scpConfig); + } + + rootMap.put(UI_FIELDS_KEY, uiFields); + + try { + return objectMapper.writeValueAsString(rootMap); + } catch (JsonProcessingException e) { + LOGGER.error("Failed to serialize UI fields to JSON", e); + return dto.getStorageResourceDescription() != null ? dto.getStorageResourceDescription() : ""; + } + } + + // Helper methods for extracting values from JSON nodes + private String getStringValue(JsonNode node, String key) { + JsonNode valueNode = node.get(key); + return valueNode != null && !valueNode.isNull() ? valueNode.asText() : null; + } + + private Integer getIntegerValue(JsonNode node, String key) { + JsonNode valueNode = node.get(key); + return valueNode != null && !valueNode.isNull() ? valueNode.asInt() : null; + } + + private Long getLongValue(JsonNode node, String key) { + JsonNode valueNode = node.get(key); + return valueNode != null && !valueNode.isNull() ? valueNode.asLong() : null; + } + + private Boolean getBooleanValue(JsonNode node, String key) { + JsonNode valueNode = node.get(key); + return valueNode != null && !valueNode.isNull() ? valueNode.asBoolean() : null; + } + + // =============================== + // JPA Entity Conversion Methods + // =============================== + + /** + * Convert LocalStorageResourceEntity (JPA) to StorageResourceDTO + */ + public StorageResourceDTO storageEntityToDTO(LocalStorageResourceEntity entity) { + if (entity == null) { + return null; + } + + StorageResourceDTO dto = new StorageResourceDTO(); + + // Core fields + dto.setStorageResourceId(entity.getStorageResourceId()); + dto.setHostName(entity.getHostName()); + dto.setStorageResourceDescription(entity.getStorageResourceDescription()); + dto.setEnabled(entity.isEnabled()); + + // Generate a name from hostname if not available + dto.setName(generateStorageResourceName(entity.getHostName(), entity.getStorageResourceDescription())); + + // Timestamps + if (entity.getCreationTime() != null) { + dto.setCreationTime(entity.getCreationTime().getTime()); + } + if (entity.getUpdateTime() != null) { + dto.setUpdateTime(entity.getUpdateTime().getTime()); + } + + // Extract UI-specific fields from description JSON + extractStorageUIFieldsFromDescription(entity.getStorageResourceDescription(), dto); + + return dto; + } + + /** + * Convert StorageResourceDTO to LocalStorageResourceEntity (JPA) + */ + public LocalStorageResourceEntity storageResourceDTOToEntity(StorageResourceDTO dto) { + if (dto == null) { + return null; + } + + LocalStorageResourceEntity entity = new LocalStorageResourceEntity(); + + // Core fields + entity.setStorageResourceId(dto.getStorageResourceId()); + entity.setHostName(dto.getHostName()); + entity.setEnabled(dto.isEnabled()); + + // Embed UI fields into description as JSON + String descriptionWithUIFields = encodeStorageUIFieldsIntoDescription(dto); + entity.setStorageResourceDescription(descriptionWithUIFields); + + return entity; + } + + /** + * Convert LocalComputeResourceEntity (JPA) to ComputeResourceDTO + */ + public ComputeResourceDTO computeEntityToDTO(LocalComputeResourceEntity entity) { + if (entity == null) { + return null; + } + + ComputeResourceDTO dto = new ComputeResourceDTO(); + + // Core fields + dto.setComputeResourceId(entity.getComputeResourceId()); + dto.setHostName(entity.getHostName()); + dto.setResourceDescription(entity.getResourceDescription()); + dto.setEnabled(entity.getEnabled() == 1); + dto.setCpuCores(entity.getCpusPerNode()); + dto.setMemoryGB(entity.getMaxMemoryPerNode()); + + // Generate a name from hostname if not available + dto.setName(generateComputeResourceName(entity.getHostName(), entity.getResourceDescription())); + + // Timestamps + if (entity.getCreationTime() != null) { + dto.setCreationTime(entity.getCreationTime().getTime()); + } + if (entity.getUpdateTime() != null) { + dto.setUpdateTime(entity.getUpdateTime().getTime()); + } + + // Extract UI-specific fields from description JSON + extractComputeUIFieldsFromDescription(entity.getResourceDescription(), dto); + + return dto; + } + + /** + * Convert ComputeResourceDTO to LocalComputeResourceEntity (JPA) + */ + public LocalComputeResourceEntity computeResourceDTOToEntity(ComputeResourceDTO dto) { + if (dto == null) { + return null; + } + + LocalComputeResourceEntity entity = new LocalComputeResourceEntity(); + + // Core fields + entity.setComputeResourceId(dto.getComputeResourceId()); + entity.setHostName(dto.getHostName()); + entity.setEnabled(dto.isEnabled() ? (short) 1 : (short) 0); + entity.setCpusPerNode(dto.getCpuCores()); + entity.setMaxMemoryPerNode(dto.getMemoryGB()); + + // Embed UI fields into description as JSON + String descriptionWithUIFields = encodeComputeUIFieldsIntoDescription(dto); + entity.setResourceDescription(descriptionWithUIFields); + + return entity; + } + + // Helper method to extract storage UI fields from JSON in description + private void extractStorageUIFieldsFromDescription(String description, StorageResourceDTO dto) { + if (description == null || !description.contains("UI_FIELDS:")) { + return; + } + + try { + // Extract JSON part after UI_FIELDS: + String jsonPart = description.substring(description.indexOf("UI_FIELDS:") + 10).trim(); + JsonNode rootNode = objectMapper.readTree(jsonPart); + + // Extract UI-specific fields + dto.setStorageType(getStringValue(rootNode, STORAGE_TYPE_KEY)); + dto.setCapacityTB(getLongValue(rootNode, CAPACITY_TB_KEY)); + dto.setAccessProtocol(getStringValue(rootNode, ACCESS_PROTOCOL_KEY)); + dto.setSupportsEncryption(getBooleanValue(rootNode, SUPPORTS_ENCRYPTION_KEY)); + dto.setSupportsVersioning(getBooleanValue(rootNode, SUPPORTS_VERSIONING_KEY)); + + // S3-specific fields + dto.setBucketName(getStringValue(rootNode, BUCKET_NAME_KEY)); + dto.setAccessKey(getStringValue(rootNode, ACCESS_KEY_KEY)); + dto.setSecretKey(getStringValue(rootNode, SECRET_KEY_KEY)); + + // SCP-specific fields + dto.setPort(getIntegerValue(rootNode, PORT_KEY)); + dto.setUsername(getStringValue(rootNode, USERNAME_KEY)); + dto.setAuthenticationMethod(getStringValue(rootNode, AUTH_METHOD_KEY)); + dto.setRemotePath(getStringValue(rootNode, REMOTE_PATH_KEY)); + + // Clean description (remove UI_FIELDS part) + String cleanDescription = description.substring(0, description.indexOf("UI_FIELDS:")).trim(); + if (cleanDescription.endsWith("\n\n")) { + cleanDescription = cleanDescription.substring(0, cleanDescription.length() - 2); + } + dto.setStorageResourceDescription(cleanDescription); + + } catch (Exception e) { + LOGGER.warn("Failed to extract storage UI fields from description", e); + } + } + + // Helper method to extract compute UI fields from JSON in description + private void extractComputeUIFieldsFromDescription(String description, ComputeResourceDTO dto) { + if (description == null || !description.contains("UI_FIELDS:")) { + return; + } + + try { + // Extract JSON part after UI_FIELDS: + String jsonPart = description.substring(description.indexOf("UI_FIELDS:") + 10).trim(); + JsonNode rootNode = objectMapper.readTree(jsonPart); + + // Extract UI-specific fields + dto.setComputeType(getStringValue(rootNode, COMPUTE_TYPE_KEY)); + dto.setOperatingSystem(getStringValue(rootNode, OPERATING_SYSTEM_KEY)); + dto.setSchedulerType(getStringValue(rootNode, SCHEDULER_TYPE_KEY)); + dto.setDataMovementProtocol(getStringValue(rootNode, DATA_MOVEMENT_PROTOCOL_KEY)); + + // Clean description (remove UI_FIELDS part) + String cleanDescription = description.substring(0, description.indexOf("UI_FIELDS:")).trim(); + if (cleanDescription.endsWith("\n\n")) { + cleanDescription = cleanDescription.substring(0, cleanDescription.length() - 2); + } + dto.setResourceDescription(cleanDescription); + + } catch (Exception e) { + LOGGER.warn("Failed to extract compute UI fields from description", e); + } + } + + // Helper method to encode storage UI fields into description + private String encodeStorageUIFieldsIntoDescription(StorageResourceDTO dto) { + StringBuilder description = new StringBuilder(); + + // Add base description + if (dto.getStorageResourceDescription() != null) { + description.append(dto.getStorageResourceDescription()); + } + + // Add UI fields as JSON + try { + Map uiFields = new HashMap<>(); + uiFields.put(STORAGE_TYPE_KEY, dto.getStorageType()); + uiFields.put(CAPACITY_TB_KEY, dto.getCapacityTB()); + uiFields.put(ACCESS_PROTOCOL_KEY, dto.getAccessProtocol()); + uiFields.put(SUPPORTS_ENCRYPTION_KEY, dto.getSupportsEncryption()); + uiFields.put(SUPPORTS_VERSIONING_KEY, dto.getSupportsVersioning()); + + // S3-specific fields + if (dto.getBucketName() != null) { + uiFields.put(BUCKET_NAME_KEY, dto.getBucketName()); + } + if (dto.getAccessKey() != null) { + uiFields.put(ACCESS_KEY_KEY, dto.getAccessKey()); + } + if (dto.getSecretKey() != null) { + uiFields.put(SECRET_KEY_KEY, dto.getSecretKey()); + } + + // SCP-specific fields + if (dto.getPort() != null) { + uiFields.put(PORT_KEY, dto.getPort()); + } + if (dto.getUsername() != null) { + uiFields.put(USERNAME_KEY, dto.getUsername()); + } + if (dto.getAuthenticationMethod() != null) { + uiFields.put(AUTH_METHOD_KEY, dto.getAuthenticationMethod()); + } + if (dto.getRemotePath() != null) { + uiFields.put(REMOTE_PATH_KEY, dto.getRemotePath()); + } + + String uiFieldsJson = objectMapper.writeValueAsString(uiFields); + description.append("\n\nUI_FIELDS: ").append(uiFieldsJson); + + } catch (Exception e) { + LOGGER.warn("Failed to encode storage UI fields", e); + } + + return description.toString(); + } + + // Helper method to encode compute UI fields into description + private String encodeComputeUIFieldsIntoDescription(ComputeResourceDTO dto) { + StringBuilder description = new StringBuilder(); + + // Add base description + if (dto.getResourceDescription() != null) { + description.append(dto.getResourceDescription()); + } + + // Add UI fields as JSON + try { + Map uiFields = new HashMap<>(); + uiFields.put(COMPUTE_TYPE_KEY, dto.getComputeType()); + uiFields.put(OPERATING_SYSTEM_KEY, dto.getOperatingSystem()); + uiFields.put(SCHEDULER_TYPE_KEY, dto.getSchedulerType()); + uiFields.put(DATA_MOVEMENT_PROTOCOL_KEY, dto.getDataMovementProtocol()); + + String uiFieldsJson = objectMapper.writeValueAsString(uiFields); + description.append("\n\nUI_FIELDS: ").append(uiFieldsJson); + + } catch (Exception e) { + LOGGER.warn("Failed to encode compute UI fields", e); + } + + return description.toString(); + } + + /** + * Generate a human-readable name for storage resource from hostname and description + */ + private String generateStorageResourceName(String hostName, String description) { + if (description != null && description.length() > 10) { + // Try to extract first line/sentence as name + String firstLine = description.split("\n")[0]; + if (firstLine.length() > 5 && firstLine.length() < 100) { + return firstLine; + } + } + + // Fallback to hostname-based name + if (hostName != null) { + return hostName.replace(".edu", "") + .replace("-", " ") + .replace(".", " "); + } + + return "Storage Resource"; + } + + /** + * Generate a human-readable name for compute resource from hostname and description + */ + private String generateComputeResourceName(String hostName, String description) { + if (description != null && description.length() > 10) { + // Try to extract first line/sentence as name + String firstLine = description.split("\n")[0]; + if (firstLine.length() > 5 && firstLine.length() < 100) { + return firstLine; + } + } + + // Fallback to hostname-based name + if (hostName != null) { + return hostName.replace(".edu", "") + .replace(".org", "") + .replace("-", " ") + .replace(".", " "); + } + + return "Compute Resource"; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java index 770220850b7..e2c8b8d326d 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java @@ -20,7 +20,6 @@ package org.apache.airavata.research.service.v2.config; import jakarta.annotation.PostConstruct; -import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -31,43 +30,34 @@ import org.apache.airavata.research.service.model.entity.Tag; import org.apache.airavata.research.service.model.repo.TagRepository; import org.apache.airavata.research.service.v2.entity.Code; -import org.apache.airavata.research.service.v2.entity.ComputeResource; -import org.apache.airavata.research.service.v2.entity.ComputeResourceQueue; -import org.apache.airavata.research.service.v2.entity.StorageResource; import org.apache.airavata.research.service.v2.repository.CodeRepository; -import org.apache.airavata.research.service.v2.repository.ComputeResourceRepository; -import org.apache.airavata.research.service.v2.repository.StorageResourceRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; +/** + * V2 Data Initializer for Code entities + * Storage resources now use airavata-api registry services (following migration.md) + */ @Component public class V2DataInitializer { private static final Logger LOGGER = LoggerFactory.getLogger(V2DataInitializer.class); - private final ComputeResourceRepository computeResourceRepository; - private final StorageResourceRepository storageResourceRepository; private final CodeRepository codeRepository; private final TagRepository tagRepository; - public V2DataInitializer(ComputeResourceRepository computeResourceRepository, - StorageResourceRepository storageResourceRepository, - CodeRepository codeRepository, + public V2DataInitializer(CodeRepository codeRepository, TagRepository tagRepository) { - this.computeResourceRepository = computeResourceRepository; - this.storageResourceRepository = storageResourceRepository; this.codeRepository = codeRepository; this.tagRepository = tagRepository; } @PostConstruct public void initializeData() { - LOGGER.info("Initializing V2 mock data for compute, storage, and code resources..."); + LOGGER.info("Initializing V2 mock data for code resources..."); try { - initializeComputeResources(); - initializeStorageResources(); initializeCodes(); LOGGER.info("V2 mock data initialization completed."); @@ -77,709 +67,262 @@ public void initializeData() { } } - private void initializeComputeResources() { - if (computeResourceRepository.count() == 0) { - LOGGER.info("Creating mock compute resources..."); - - List computeResources = List.of( - new ComputeResource( - "Bridges-2 Supercomputer", - "Advanced high-performance computing cluster at Pittsburgh Supercomputing Center with GPU acceleration for AI workloads.", - "bridges2.psc.edu", - "HPC", - 1280, - 2560, - "CentOS 7", - "SLURM", - "Features GPU nodes for machine learning, regular memory and extreme memory configurations. Maximum job time: 48 hours.", - "Pittsburgh Supercomputing Center", - "hpcuser", - 22, - "SSH_KEY", - "/home/hpcuser", - "SLURM", - "SCP" - ), - - new ComputeResource( - "Expanse Supercomputer", - "SDSC's newest supercomputer designed for scientific computing with specialized GPU nodes for machine learning and AI research.", - "expanse.sdsc.edu", - "HPC", - 1408, - 2816, - "CentOS 8", - "SLURM", - "CPU and GPU partitions available. Optimized for parallel computing and machine learning workloads.", - "San Diego Supercomputer Center", - "expanseuser", - 22, - "SSH_KEY", - "/expanse/lustre/scratch", - "SLURM", - "SCP" - ), - - new ComputeResource( - "Anvil Cluster", - "Purdue University's advanced computing cluster with high-memory nodes and specialized hardware for data analytics.", - "anvil.rcac.purdue.edu", - "HPC", - 1000, - 4000, - "Red Hat Enterprise Linux 8", - "SLURM", - "High-memory nodes (1.5TB RAM), GPU nodes with V100 and A100 cards for deep learning applications.", - "Purdue University RCAC", - "anviluser", - 22, - "SSH_KEY", - "/anvil/scratch", - "SLURM", - "SCP" - ), - - new ComputeResource( - "Frontera Supercomputer", - "Leadership-class supercomputer at TACC for large-scale computational research and simulation.", - "frontera.tacc.utexas.edu", - "HPC", - 8008, - 16000, - "CentOS 7", - "SLURM", - "Leadership computing facility with specialized queues for different workload types. Maximum allocation: 3M core-hours.", - "Texas Advanced Computing Center", - "fronterauser", - 22, - "SSH_KEY", - "/scratch1/projects", - "SLURM", - "SCP" - ), - - new ComputeResource( - "AWS EC2 Compute Cloud", - "Scalable cloud computing platform with on-demand instance provisioning and auto-scaling capabilities.", - "amazonaws.com", - "Cloud", - 9999, - 99999, - "Amazon Linux 2", - "Cloud Native", - "Pay-as-you-go pricing model with various instance types (CPU, memory, GPU optimized). Global availability zones.", - "Amazon Web Services", - "ec2-user", - 22, - "SSH_KEY", - "/home/ec2-user", - "Cloud Native", - "SCP" - ), - - new ComputeResource( - "Google Cloud Compute Engine", - "High-performance virtual machines with custom machine types and specialized accelerators for AI/ML workloads.", - "compute.googleapis.com", - "Cloud", - 8888, - 88888, - "Ubuntu 20.04 LTS", - "Cloud Native", - "Preemptible instances available for cost savings. TPUs available for machine learning acceleration.", - "Google Cloud Platform", - "gceuser", - 22, - "SSH_KEY", - "/home/gceuser", - "Cloud Native", - "SCP" - ), - - new ComputeResource( - "XSEDE Comet", - "GPU-accelerated supercomputer optimized for data-intensive computing and machine learning applications.", - "comet.sdsc.edu", - "HPC", - 1980, - 2640, - "CentOS 7", - "SLURM", - "72 GPU nodes with K80 cards, high-speed interconnect, and parallel file systems for data-intensive research.", - "San Diego Supercomputer Center", - "cometuser", - 22, - "SSH_KEY", - "/oasis/scratch/comet", - "SLURM", - "SCP" - ), - - new ComputeResource( - "Jetstream2 Cloud", - "National cyberinfrastructure providing on-demand virtual machines for academic research computing.", - "jetstream-cloud.org", - "Cloud", - 2000, - 8000, - "Various Linux Distributions", - "OpenStack", - "Self-service cloud environment with support for containers, Kubernetes, and Jupyter notebooks.", - "Indiana University & TACC", - "jetstream", - 22, - "SSH_KEY", - "/home/jetstream", - "OpenStack", - "SCP" - ), - - new ComputeResource( - "NERSC Perlmutter", - "Exascale-class supercomputer with GPU acceleration designed for scientific computing and AI convergence.", - "perlmutter.nersc.gov", - "HPC", - 6159, - 4915, - "SUSE Linux Enterprise", - "SLURM", - "A100 GPU nodes optimized for mixed-precision computing. Advanced interconnect and parallel file systems.", - "National Energy Research Scientific Computing Center", - "perlmutter", - 22, - "SSH_KEY", - "/global/cfs/cdirs", - "SLURM", - "SCP" - ) - ); - - computeResourceRepository.saveAll(computeResources); - LOGGER.info("Created {} compute resources", computeResources.size()); - - // Initialize queues for each compute resource - initializeComputeResourceQueues(computeResources); - } - } - - private void initializeComputeResourceQueues(List computeResources) { - LOGGER.info("Creating mock compute resource queues..."); - - for (ComputeResource computeResource : computeResources) { - List queues = new ArrayList<>(); - - // Create different queue configurations based on resource type - if (computeResource.getComputeType().equals("HPC")) { - // Standard HPC queues - queues.add(new ComputeResourceQueue( - "GPU queue", - "High-priority queue for GPU-accelerated workloads", - 2880, // 48 hours - 32, - 1024, - 100, - 64, - 1, - 64, - 60, // 1 hour default - "#SBATCH --partition=gpu\n#SBATCH --gres=gpu:4", - false - )); - - queues.add(new ComputeResourceQueue( - "Compute queue", - "Standard compute queue for CPU-intensive workloads", - 1440, // 24 hours - 128, - 2048, - 500, - 48, - 2, - 96, - 120, // 2 hours default - "#SBATCH --partition=compute", - true // Default queue - )); - - queues.add(new ComputeResourceQueue( - "Debug queue", - "Quick debug queue with shorter runtime limits", - 30, // 30 minutes - 4, - 128, - 10, - 24, - 1, - 24, - 15, // 15 minutes default - "#SBATCH --partition=debug\n#SBATCH --qos=debug", - false - )); - - queues.add(new ComputeResourceQueue( - "GPU shared queue", - "Shared GPU resources for smaller workloads", - 720, // 12 hours - 8, - 256, - 50, - 32, - 1, - 32, - 30, // 30 minutes default - "#SBATCH --partition=gpu-shared\n#SBATCH --gres=gpu:1", - false - )); - - } else if (computeResource.getComputeType().equals("Cloud")) { - // Cloud-based queues - queues.add(new ComputeResourceQueue( - "On-demand", - "On-demand instances with flexible resource allocation", - 10080, // 7 days - 1000, - 10000, - 1000, - 96, - 1, - 4, - 60, // 1 hour default - "# Cloud-native auto-scaling enabled", - true // Default queue - )); - - queues.add(new ComputeResourceQueue( - "Spot instances", - "Cost-optimized spot instances for fault-tolerant workloads", - 2880, // 48 hours - 500, - 5000, - 500, - 96, - 1, - 2, - 30, // 30 minutes default - "# Spot instance with automatic checkpointing", - false - )); - } - - // Set the compute resource relationship and save - for (ComputeResourceQueue queue : queues) { - queue.setComputeResource(computeResource); - } - - computeResource.setQueues(queues); - } - - // Save all compute resources with their queues - computeResourceRepository.saveAll(computeResources); - - LOGGER.info("Created queues for {} compute resources", computeResources.size()); - } - - private void initializeStorageResources() { - if (storageResourceRepository.count() == 0) { - LOGGER.info("Creating mock storage resources..."); - - List storageResources = List.of( - // S3 Storage Resources - new StorageResource( - "AWS S3 Research Bucket", - "Production S3 bucket for research data with automated lifecycle management and encryption.", - "S3", - "https://s3.amazonaws.com", - "research-data-bucket-01", - "AKIAIOSFODNN7EXAMPLE", - "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", - "Amazon Web Services" - ), - - new StorageResource( - "Google Cloud Storage Bucket", - "Multi-regional cloud storage bucket with integrated AI/ML data processing capabilities.", - "S3", - "https://storage.googleapis.com", - "ml-datasets-bucket", - "GOOGABCDEFG123456789", - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk123", - "Google Cloud Platform" - ), - - new StorageResource( - "MinIO Research Storage", - "Self-hosted S3-compatible object storage for sensitive research data with local control.", - "S3", - "https://minio.research.university.edu", - "private-research-data", - "minioadmin", - "minioadmin123", - "University Research Computing" - ), - - // SCP Storage Resources - new StorageResource( - "HPC Cluster Storage", - "High-performance shared file system accessible via SCP for computational workflows.", - "cluster.hpc.university.edu", - "SCP", - 22, - "researcher01", - "SSH_KEY", - "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA7yn3bRHQ...", - "/shared/research/datasets", - "University HPC Center" - ), - - new StorageResource( - "XSEDE Stampede2 Storage", - "TACC Stampede2 supercomputer storage system with high-speed parallel file system access.", - "stampede2.tacc.utexas.edu", - "SCP", - 22, - "tg-username123", - "SSH_KEY", - "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA8vKqM...", - "/work/projects/research-data", - "Texas Advanced Computing Center" - ), - - new StorageResource( - "NERSC Cori Storage", - "NERSC Cori supercomputer storage with specialized file systems for scientific computing.", - "cori.nersc.gov", - "SCP", - 22, - "nersc_user", - "PASSWORD", - null, - "/global/cscratch1/sd/username", - "National Energy Research Scientific Computing Center" - ), - - // Mixed Storage Types - new StorageResource( - "Azure Blob Storage", - "Microsoft Azure blob storage with hot and cool tier options for research data archival.", - "S3", - "https://researchstorage.blob.core.windows.net", - "research-container", - "storage_account_name", - "storage_account_key_here_very_long_key", - "Microsoft Azure" - ), - - new StorageResource( - "Laboratory Compute Server", - "Local laboratory server with direct SCP access for instrument data and analysis results.", - "labserver.biology.university.edu", - "SCP", - 2222, - "labuser", - "SSH_KEY", - "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA1qaz2...", - "/data/experiments/2024", - "University Biology Department" - ), - - new StorageResource( - "IBM Cloud Object Storage", - "Enterprise-grade object storage with S3-compatible API and advanced data protection features.", - "S3", - "https://s3.us-south.cloud-object-storage.ibm.com", - "research-cos-bucket", - "ibm_cos_access_key_id", - "ibm_cos_secret_access_key_value", - "IBM Cloud" - ) - ); - - storageResourceRepository.saveAll(storageResources); - LOGGER.info("Created {} storage resources", storageResources.size()); - } - } - private void initializeCodes() { if (codeRepository.count() == 0) { LOGGER.info("Creating mock code resources..."); - // Sample data configurations + // Research-focused sample data based on v1 patterns and real deployments CodeData[] codeDataArray = { - // Model-type codes + // Neuroscience Research Models (inspired by v1 neurodata25 projects) new CodeData( - "COVID-19 Chest X-ray Classification Model", - "Deep learning model for automatic detection of COVID-19 pneumonia from chest X-ray images using ResNet-50 architecture with transfer learning.", + "Bio-realistic Cortical Circuit Simulation Model", + "Running the AllenAI V1 model for bio-realistic multiscale simulations of cortical circuits with thalamacortical and background inputs", "MODEL", "Python", - "TensorFlow", - Set.of("dr.sarah.medical@stanford.edu", "alex.vision@mit.edu"), - Set.of("medical", "computer-vision", "covid-19", "deep-learning", "classification"), - "covid_xray_classifier_v2.1", - "2.1", + "NEURON", + Set.of("anton.arkhipov@alleninstitute.org", "laura.green@alleninstitute.org"), + Set.of("neurodata25", "allenai", "visual-cortex", "neuroscience", "simulation"), + "allenai_v1_model.pkl", + "1.0", null, null, - "Pre-trained model weights available. Compatible with standard ML pipelines." + "Bio-realistic cortical circuit model with thalamacortical inputs. Part of NeuroData25 initiative." ), new CodeData( - "Financial Fraud Detection Model", - "Machine learning ensemble model combining XGBoost and Random Forest for real-time credit card fraud detection with 99.2% accuracy.", + "Apache Cerebrum Computational Model", + "Constructing computational neuroscience models from large public databases and brain atlases using Apache Airavata middleware", "MODEL", "Python", - "Scikit-learn", - Set.of("mike.finance@jpmorgan.com", "lisa.ml@visa.com"), - Set.of("finance", "fraud-detection", "machine-learning", "ensemble", "xgboost"), - "fraud_detector_ensemble_v1.3", - "1.3", + "Apache Airavata", + Set.of("sriram.chockalingam@apache.org"), + Set.of("neurodata25", "apache", "cerebrum", "brain-atlases", "computational-neuroscience"), + "cerebrum_brain_model_v2.h5", + "2.0", null, null, - "Pre-trained model weights available. Compatible with standard ML pipelines." + "Large-scale brain modeling framework using public neuroscience databases." ), new CodeData( - "Protein Folding Prediction Model", - "AlphaFold2-inspired neural network for predicting 3D protein structures from amino acid sequences using attention mechanisms.", + "Biologically Constrained RNN Model", + "Biologically constrained recurrent neural network via Dale's backpropagation and topologically-informed pruning for neural computation", "MODEL", - "Python", + "Python", "PyTorch", - Set.of("prof.chen@deepmind.com", "bio.researcher@harvard.edu"), - Set.of("bioinformatics", "protein-folding", "deep-learning", "attention", "alphafold"), - "protein_fold_predictor_v3.0", - "3.0", + Set.of("hannah.choi@gatech.edu", "aishwarya.balwani@gatech.edu"), + Set.of("neurodata25", "hchoilab", "biological-rnn", "dale-principle", "neural-networks"), + "biological_rnn_trained.pth", + "1.2", null, null, - "Pre-trained model weights available. Compatible with standard ML pipelines." + "Biologically plausible RNN with Dale's law constraints and topological pruning." ), - // Notebook-type codes + // Computational Chemistry Models (inspired by SQL dump applications) new CodeData( - "Cybersecurity Threat Analysis Notebook", - "Comprehensive Jupyter notebook for analyzing network traffic patterns and identifying potential cybersecurity threats using statistical analysis.", - "NOTEBOOK", + "PSI4 Quantum Chemistry Model", + "OPENMP Psi4 application for ab initio quantum chemistry programs designed for efficient, high-accuracy simulations of molecular properties", + "MODEL", "Python", + "PSI4", + Set.of("quantum.team@psi4.org", "ccguser@chemistry.org"), + Set.of("quantum-chemistry", "ab-initio", "molecular-simulation", "psi4", "computational-chemistry"), + "psi4_optimized_model.wfn", + "1.8", null, - Set.of("security.analyst@cisco.com", "threat.hunter@crowdstrike.com"), - Set.of("cybersecurity", "threat-analysis", "network-security", "statistical-analysis", "jupyter"), null, + "High-accuracy quantum chemistry calculations with OPENMP parallelization." + ), + + new CodeData( + "AlphaFold2 Protein Structure Model", + "Protein structure prediction using locally deployed AlphaFold2 singularity container for accurate protein folding prediction", + "MODEL", + "Python", + "JAX", + Set.of("deepmind.team@google.com", "scigap@alphafold.org"), + Set.of("protein-folding", "alphafold", "structural-biology", "deep-learning", "bioinformatics"), + "alphafold2_weights.pkl", + "2.3", null, - "/notebooks/cybersecurity/threat_analysis.ipynb", null, - "Interactive Jupyter notebook with step-by-step analysis and visualizations." + "State-of-the-art protein structure prediction using AlphaFold2 architecture." ), + // Research Notebooks (based on real scientific workflows) new CodeData( - "Climate Data Visualization Notebook", - "Interactive data visualization notebook for climate change analysis using NOAA datasets with advanced plotting and statistical modeling.", + "Whole-Brain Sleep Dynamics Analysis", + "Jupyter notebook for analyzing spatio-temporal dynamics of sleep in large-scale brain models during awake and sleep states", "NOTEBOOK", "Python", - null, - Set.of("climate.scientist@noaa.gov", "data.viz@nasa.gov"), - Set.of("climate-science", "data-visualization", "environmental", "statistical-modeling", "matplotlib"), + "Jupyter", + Set.of("maxim.bazhenov@ucsd.edu", "gabriela.navas@ucsd.edu"), + Set.of("neurodata25", "bazhlab", "whole-brain", "sleep-dynamics", "neuroscience"), + "sleep_dynamics_analysis.ipynb", + "3.1", null, null, - "/notebooks/climate/climate_visualization.ipynb", - null, - "Interactive Jupyter notebook with step-by-step analysis and visualizations." + "Comprehensive analysis of sleep-related brain activity patterns using large-scale modeling." ), new CodeData( - "NLP Sentiment Analysis Notebook", - "End-to-end natural language processing pipeline for sentiment analysis of social media data using transformer models and BERT.", + "One-hot HMM-GLM Brain State Discovery", + "Implementation of One-hot Generalized Linear Model for switching brain state discovery, reproducing ICLR 2024 paper findings", "NOTEBOOK", "Python", + "JAX", + Set.of("anqi.wu@nyu.edu", "chengrui.li@nyu.edu"), + Set.of("neurodata25", "brainml", "hmm-glm", "brain-states", "machine-learning"), + "onehot_hmmglm_analysis.ipynb", + "1.0", null, - Set.of("nlp.researcher@google.com", "sentiment.expert@twitter.com"), - Set.of("nlp", "sentiment-analysis", "transformers", "bert", "social-media"), null, + "Advanced statistical modeling for brain state identification using GLM framework." + ), + + new CodeData( + "NAMD Molecular Dynamics Workflow", + "Comprehensive molecular dynamics simulation workflow using NAMD with GPU acceleration for protein-ligand interactions", + "NOTEBOOK", + "Tcl", + "NAMD", + Set.of("md.researcher@illinois.edu", "namd.support@ks.uiuc.edu"), + Set.of("molecular-dynamics", "namd", "protein-simulation", "gpu-computing", "hpc"), + "namd_md_workflow.ipynb", + "2.14", null, - "/notebooks/nlp/sentiment_analysis.ipynb", null, - "Interactive Jupyter notebook with step-by-step analysis and visualizations." + "Production molecular dynamics workflows with NAMD 2.14 and GPU support." ), - // Repository-type codes + // Research Code Repositories (following v1 GitHub pattern) new CodeData( - "Distributed Machine Learning Framework", - "Open-source framework for distributed machine learning across multiple compute nodes with fault tolerance and auto-scaling capabilities.", + "Neural Oscillators Computing Framework", + "A comprehensive framework for computing with neural oscillators, including speech processing demos and neuromorphic computing applications", "REPOSITORY", "Python", - "PyTorch", - Set.of("distributed.ml@uber.com", "framework.dev@netflix.com"), - Set.of("distributed-computing", "machine-learning", "framework", "pytorch", "scalability"), + "NumPy", + Set.of("nabil.imam@intel.com", "nand.chandravadia@intel.com"), + Set.of("neurodata25", "imamlab", "neural-oscillators", "neuromorphic", "speech-processing"), null, null, - null, - "https://github.com/ml-distributed/framework", - "Full source code repository with documentation, tests, and CI/CD pipeline." + "https://github.com/cyber-shuttle/imamlab-neural-oscillators", + "main", + "Neural oscillator-based computing for neuromorphic applications and speech processing." ), new CodeData( - "Quantum Computing Algorithms Library", - "Comprehensive library of quantum computing algorithms implemented in Qiskit with educational examples and benchmarking tools.", + "Torch Brain and TemporalData Toolkit", + "Scaling up neural data analysis with torch_brain and temporaldata libraries for large-scale neuroscience data processing", "REPOSITORY", - "Python", - "Qiskit", - Set.of("quantum.researcher@ibm.com", "algorithms.expert@google.com"), - Set.of("quantum-computing", "algorithms", "qiskit", "benchmarking", "education"), - null, + "Python", + "PyTorch", + Set.of("eva.dyer@gatech.edu", "vinam.arora@gatech.edu", "mahato.shivashriganesh@gatech.edu"), + Set.of("neurodata25", "nerdslab", "torch-brain", "temporaldata", "neuroscience"), null, null, - "https://github.com/quantum-algorithms/qiskit-library", - "Full source code repository with documentation, tests, and CI/CD pipeline." + "https://github.com/cyber-shuttle/neurodata25_torchbrain_notebooks", + "main", + "Advanced neural data analysis tools with PyTorch integration for large-scale processing." ), new CodeData( - "Time Series Forecasting Toolkit", - "Advanced time series analysis and forecasting toolkit with support for ARIMA, LSTM, and Prophet models for financial and IoT data.", + "NetFormer Neural Connectivity Model", + "Running the NetFormer model to bridge the gap between structure and function in the brain using transformer architectures", "REPOSITORY", "Python", - "TensorFlow", - Set.of("time.series@bloomberg.com", "forecasting.expert@amazon.com"), - Set.of("time-series", "forecasting", "arima", "lstm", "prophet", "financial"), + "Transformers", + Set.of("lu.mi@neuroaihub.org"), + Set.of("neurodata25", "neuroaihub", "netformer", "neural-connectivity", "transformers"), null, null, - null, - "https://github.com/timeseries-toolkit/forecasting", - "Full source code repository with documentation, tests, and CI/CD pipeline." + "https://github.com/cyber-shuttle/neuroaihub-netformer", + "main", + "Transformer-based analysis of neural connectivity patterns in brain networks." ) }; + // Create codes from sample data for (CodeData codeData : codeDataArray) { - try { - Code code = createCodeWithTags(codeData); - codeRepository.save(code); - } catch (Exception e) { - LOGGER.error("Error creating code '{}': {}", codeData.name, e.getMessage(), e); - } + Code code = createCodeFromData(codeData); + codeRepository.save(code); } LOGGER.info("Created {} code resources", codeDataArray.length); } } - /** - * Helper method to create Code entity with proper Tag associations - */ - private Code createCodeWithTags(CodeData codeData) { - // Create or get existing Tag entities - Set tagEntities = getOrCreateTags(codeData.tagStrings); - - // Create Code entity using the constructor - Code code = new Code(codeData.name, codeData.description, codeData.codeType, - codeData.programmingLanguage, codeData.framework, - codeData.authors, tagEntities); + private Code createCodeFromData(CodeData data) { + Code code = new Code(); + code.setName(data.name); + code.setDescription(data.description); + code.setCodeType(data.codeType); + code.setProgrammingLanguage(data.programmingLanguage); + code.setFramework(data.framework); + code.setVersion(data.version); + code.setFileName(data.fileName); + code.setRepositoryUrl(data.repositoryUrl); + code.setBranch(data.branch); + code.setAdditionalInfo(data.additionalInfo); - // Set enum-based fields with proper defaults - code.setStatus(StatusEnum.VERIFIED); - code.setState(StateEnum.ACTIVE); + // Set default v1 Resource fields (inherited) code.setPrivacy(PrivacyEnum.PUBLIC); - - // Set random star count for demonstration - int starCount = (int) (Math.random() * 1000) + 10; - // Note: starCount functionality handled separately in v1 star system - // code.setStarCount(starCount); // Removed - v2 entities don't have starCount field - - // Set code-specific fields - if (codeData.applicationInterfaceId != null) { - code.setApplicationInterfaceId(codeData.applicationInterfaceId); - } - if (codeData.version != null) { - code.setVersion(codeData.version); - } - if (codeData.notebookPath != null) { - code.setNotebookPath(codeData.notebookPath); - } - if (codeData.repositoryUrl != null) { - code.setRepositoryUrl(codeData.repositoryUrl); - } - if (codeData.additionalInfo != null) { - code.setAdditionalInfo(codeData.additionalInfo); - } - - // Set header image - use a default for now - code.setHeaderImage("https://via.placeholder.com/400x200?text=" + codeData.codeType); - - // Add dependencies based on programming language and framework - addDependencies(code); + code.setState(StateEnum.ACTIVE); + code.setStatus(StatusEnum.VERIFIED); + code.setAuthors(new HashSet<>(data.authors)); + code.setTags(getOrCreateTags(data.tags)); + code.setHeaderImage(""); // Default empty header image return code; } - /** - * Helper method to get or create Tag entities from tag strings - */ - private Set getOrCreateTags(Set tagStrings) { - Set tagEntities = new HashSet<>(); - - for (String tagString : tagStrings) { - Optional existingTag = Optional.ofNullable(tagRepository.findByValue(tagString)); - if (existingTag.isPresent()) { - tagEntities.add(existingTag.get()); + private Set getOrCreateTags(Set tagNames) { + Set tags = new HashSet<>(); + for (String tagName : tagNames) { + Tag existingTag = tagRepository.findByValue(tagName); + if (existingTag != null) { + tags.add(existingTag); } else { - // Create new tag Tag newTag = new Tag(); - newTag.setValue(tagString); + newTag.setValue(tagName); Tag savedTag = tagRepository.save(newTag); - tagEntities.add(savedTag); - LOGGER.debug("Created new tag: {}", tagString); + tags.add(savedTag); } } - - return tagEntities; + return tags; } - /** - * Helper method to add dependencies based on programming language and framework - */ - private void addDependencies(Code code) { - if ("Python".equals(code.getProgrammingLanguage())) { - code.getDependencies().addAll(Set.of("numpy", "pandas", "matplotlib")); - - String framework = code.getFramework(); - if ("TensorFlow".equals(framework)) { - code.getDependencies().addAll(Set.of("tensorflow>=2.8.0", "keras")); - } else if ("PyTorch".equals(framework)) { - code.getDependencies().addAll(Set.of("torch>=1.12.0", "torchvision")); - } else if ("Scikit-learn".equals(framework)) { - code.getDependencies().addAll(Set.of("scikit-learn>=1.1.0", "joblib")); - } else if ("Qiskit".equals(framework)) { - code.getDependencies().addAll(Set.of("qiskit>=0.39.0", "qiskit-aer")); - } - } - } - /** - * Data structure to hold code initialization data - */ + // Helper class for organizing sample data private static class CodeData { final String name; final String description; - final String codeType; + final String codeType; // MODEL, NOTEBOOK, REPOSITORY final String programmingLanguage; final String framework; final Set authors; - final Set tagStrings; - final String applicationInterfaceId; + final Set tags; + final String fileName; // For models and notebooks final String version; - final String notebookPath; - final String repositoryUrl; + final String repositoryUrl; // For repositories + final String branch; // For repositories final String additionalInfo; - CodeData(String name, String description, String codeType, String programmingLanguage, - String framework, Set authors, Set tagStrings, - String applicationInterfaceId, String version, String notebookPath, - String repositoryUrl, String additionalInfo) { + public CodeData(String name, String description, String codeType, String programmingLanguage, + String framework, Set authors, Set tags, String fileName, + String version, String repositoryUrl, String branch, String additionalInfo) { this.name = name; this.description = description; this.codeType = codeType; this.programmingLanguage = programmingLanguage; this.framework = framework; - this.authors = authors != null ? authors : new HashSet<>(); - this.tagStrings = tagStrings != null ? tagStrings : new HashSet<>(); - this.applicationInterfaceId = applicationInterfaceId; + this.authors = authors; + this.tags = tags; + this.fileName = fileName; this.version = version; - this.notebookPath = notebookPath; this.repositoryUrl = repositoryUrl; + this.branch = branch; this.additionalInfo = additionalInfo; } } diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java index bb54c6d2430..bc562c46ca4 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java @@ -23,18 +23,11 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.List; -import java.util.Optional; -import org.apache.airavata.research.service.enums.PrivacyEnum; -import org.apache.airavata.research.service.enums.StateEnum; -import org.apache.airavata.research.service.v2.entity.ComputeResource; -import org.apache.airavata.research.service.v2.repository.ComputeResourceRepository; -import org.apache.airavata.research.service.v2.service.ComputeResourceService; +import org.apache.airavata.research.service.dto.ComputeResourceDTO; +import org.apache.airavata.research.service.handler.LocalComputeResourceHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindingResult; @@ -54,59 +47,57 @@ public class ComputeResourceController { private static final Logger LOGGER = LoggerFactory.getLogger(ComputeResourceController.class); - private static final PrivacyEnum PUBLIC_PRIVACY = PrivacyEnum.PUBLIC; - private static final StateEnum ACTIVE_STATE = StateEnum.ACTIVE; - private final ComputeResourceRepository computeResourceRepository; - private final ComputeResourceService computeResourceService; + @Autowired + private LocalComputeResourceHandler localComputeResourceHandler; - public ComputeResourceController(ComputeResourceRepository computeResourceRepository, - ComputeResourceService computeResourceService) { - this.computeResourceRepository = computeResourceRepository; - this.computeResourceService = computeResourceService; - } - - @Operation(summary = "Get all public compute resources with pagination") + @Operation(summary = "Get all public compute resources") @GetMapping("/public") - public ResponseEntity> getComputeResources( - @RequestParam(value = "pageNumber", defaultValue = "0") int pageNumber, - @RequestParam(value = "pageSize", defaultValue = "10") int pageSize, - @RequestParam(value = "nameSearch", required = false) String nameSearch, - @RequestParam(value = "tag", required = false) String[] tags) { - - LOGGER.info("Getting compute resources - page: {}, size: {}, search: {}", pageNumber, pageSize, nameSearch); + public ResponseEntity> getComputeResources( + @RequestParam(value = "nameSearch", required = false) String nameSearch) { - Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending()); - Page resources; + LOGGER.info("Getting compute resources - search: {}", nameSearch); - if (nameSearch != null && !nameSearch.trim().isEmpty()) { - resources = computeResourceService.searchComputeResources(nameSearch, PUBLIC_PRIVACY, ACTIVE_STATE, pageable); - } else { - resources = computeResourceService.getComputeResources(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); + try { + List resources; + + if (nameSearch != null && !nameSearch.trim().isEmpty()) { + resources = localComputeResourceHandler.searchComputeResources(nameSearch); + } else { + resources = localComputeResourceHandler.getAllComputeResources(); + } + + LOGGER.info("Found {} compute resources", resources.size()); + return ResponseEntity.ok(resources); + } catch (Exception e) { + LOGGER.error("Failed to get compute resources: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } - - LOGGER.info("Found {} compute resources", resources.getTotalElements()); - return ResponseEntity.ok(resources); } @Operation(summary = "Get compute resource by ID") @GetMapping("/public/{id}") - public ResponseEntity getComputeResourceById(@PathVariable("id") String id) { + public ResponseEntity getComputeResourceById(@PathVariable("id") String id) { LOGGER.info("Getting compute resource by ID: {}", id); - Optional resource = computeResourceService.getComputeResourceById(id); - if (resource.isPresent()) { - return ResponseEntity.ok(resource.get()); - } else { - LOGGER.warn("Compute resource not found with ID: {}", id); - return ResponseEntity.notFound().build(); + try { + ComputeResourceDTO resource = localComputeResourceHandler.getComputeResource(id); + return ResponseEntity.ok(resource); + } catch (RuntimeException e) { + if (e.getMessage().contains("not found")) { + LOGGER.warn("Compute resource not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } else { + LOGGER.error("Error getting compute resource {}: {}", id, e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } } } @Operation(summary = "Create new compute resource") @PostMapping("/") - public ResponseEntity createComputeResource(@Valid @RequestBody ComputeResource computeResource, BindingResult bindingResult) { - LOGGER.info("Creating new compute resource: {}", computeResource.getName()); + public ResponseEntity createComputeResource(@Valid @RequestBody ComputeResourceDTO computeResourceDTO, BindingResult bindingResult) { + LOGGER.info("Creating new compute resource: {}", computeResourceDTO.getHostName()); // Validation error handling if (bindingResult.hasErrors()) { @@ -120,22 +111,15 @@ public ResponseEntity createComputeResource(@Valid @RequestBody ComputeResour try { // Set default values for fields that might be null - if (computeResource.getCpuCores() == null) { - computeResource.setCpuCores(1); // Default to 1 core - } - if (computeResource.getMemoryGB() == null) { - computeResource.setMemoryGB(1); // Default to 1 GB - } - if (computeResource.getPrivacy() == null) { - computeResource.setPrivacy(PUBLIC_PRIVACY); + if (computeResourceDTO.getCpuCores() == null) { + computeResourceDTO.setCpuCores(1); // Default to 1 core } - if (computeResource.getState() == null) { - computeResource.setState(ACTIVE_STATE); + if (computeResourceDTO.getMemoryGB() == null) { + computeResourceDTO.setMemoryGB(1); // Default to 1 GB } - // Note: starCount functionality handled separately in v1 star system - ComputeResource savedResource = computeResourceService.createComputeResource(computeResource); - LOGGER.info("Created compute resource with ID: {}", savedResource.getId()); + ComputeResourceDTO savedResource = localComputeResourceHandler.createComputeResource(computeResourceDTO); + LOGGER.info("Created compute resource with ID: {}", savedResource.getComputeResourceId()); return ResponseEntity.status(HttpStatus.CREATED).body(savedResource); } catch (Exception e) { @@ -147,7 +131,7 @@ public ResponseEntity createComputeResource(@Valid @RequestBody ComputeResour @Operation(summary = "Update compute resource") @PutMapping("/{id}") - public ResponseEntity updateComputeResource(@PathVariable("id") String id, @Valid @RequestBody ComputeResource computeResource, BindingResult bindingResult) { + public ResponseEntity updateComputeResource(@PathVariable("id") String id, @Valid @RequestBody ComputeResourceDTO computeResourceDTO, BindingResult bindingResult) { LOGGER.info("Updating compute resource with ID: {}", id); // Validation error handling @@ -161,13 +145,7 @@ public ResponseEntity updateComputeResource(@PathVariable("id") String id, @V } try { - Optional updatedResourceOpt = computeResourceService.updateComputeResource(id, computeResource); - if (!updatedResourceOpt.isPresent()) { - LOGGER.warn("Compute resource not found with ID: {}", id); - return ResponseEntity.notFound().build(); - } - - ComputeResource updatedResource = updatedResourceOpt.get(); + ComputeResourceDTO updatedResource = localComputeResourceHandler.updateComputeResource(id, computeResourceDTO); LOGGER.info("Successfully updated compute resource with ID: {}", id); return ResponseEntity.ok(updatedResource); @@ -184,12 +162,7 @@ public ResponseEntity deleteComputeResource(@PathVariable("id") String id) { LOGGER.info("Deleting compute resource with ID: {}", id); try { - boolean deleted = computeResourceService.deleteComputeResource(id); - if (!deleted) { - LOGGER.warn("Compute resource not found with ID: {}", id); - return ResponseEntity.notFound().build(); - } - + localComputeResourceHandler.deleteComputeResource(id); LOGGER.info("Successfully deleted compute resource with ID: {}", id); return ResponseEntity.ok().body("Compute resource deleted successfully"); } catch (Exception e) { @@ -201,30 +174,19 @@ public ResponseEntity deleteComputeResource(@PathVariable("id") String id) { @Operation(summary = "Search compute resources by keyword") @GetMapping("/search") - public ResponseEntity> searchComputeResources( + public ResponseEntity> searchComputeResources( @RequestParam(value = "keyword") String keyword) { LOGGER.info("Searching compute resources with keyword: {}", keyword); - List resources = computeResourceRepository - .findByNameContainingIgnoreCaseAndPrivacyAndState(keyword, PUBLIC_PRIVACY, ACTIVE_STATE); - - LOGGER.info("Found {} compute resources matching keyword: {}", resources.size(), keyword); - return ResponseEntity.ok(resources); - } - - @Operation(summary = "Get compute resources by type") - @GetMapping("/type/{computeType}") - public ResponseEntity> getComputeResourcesByType( - @PathVariable("computeType") String computeType) { - - LOGGER.info("Getting compute resources by type: {}", computeType); - - List resources = computeResourceRepository - .findByComputeTypeAndPrivacyAndState(computeType, PUBLIC_PRIVACY, ACTIVE_STATE); - - LOGGER.info("Found {} compute resources of type: {}", resources.size(), computeType); - return ResponseEntity.ok(resources); + try { + List resources = localComputeResourceHandler.searchComputeResources(keyword); + LOGGER.info("Found {} compute resources matching keyword: {}", resources.size(), keyword); + return ResponseEntity.ok(resources); + } catch (Exception e) { + LOGGER.error("Error searching compute resources: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } } @Operation(summary = "Star/unstar a compute resource") @@ -233,10 +195,7 @@ public ResponseEntity starComputeResource(@PathVariable("id") String id LOGGER.info("Toggling star for compute resource with ID: {}", id); try { - Optional resourceOpt = computeResourceRepository.findByIdWithCollections(id); - if (resourceOpt.isPresent()) { - ComputeResource resource = resourceOpt.get(); - + if (localComputeResourceHandler.existsComputeResource(id)) { // TODO: Implement proper v1 ResourceStar system integration // For now, return simple toggle response LOGGER.info("Star toggle requested for compute resource: {} (simplified implementation)", id); @@ -257,9 +216,7 @@ public ResponseEntity checkComputeResourceStarred(@PathVariable("id") S LOGGER.info("Checking if compute resource is starred: {}", id); try { - Optional resourceOpt = computeResourceRepository.findByIdWithCollections(id); - if (resourceOpt.isPresent()) { - ComputeResource resource = resourceOpt.get(); + if (localComputeResourceHandler.existsComputeResource(id)) { // TODO: Implement proper v1 ResourceStar system integration LOGGER.info("Star status check for compute resource: {} (simplified implementation)", id); return ResponseEntity.ok(false); @@ -279,8 +236,7 @@ public ResponseEntity getComputeResourceStarCount(@PathVariable("id") S LOGGER.info("Getting star count for compute resource: {}", id); try { - Optional resourceOpt = computeResourceRepository.findByIdWithCollections(id); - if (resourceOpt.isPresent()) { + if (localComputeResourceHandler.existsComputeResource(id)) { // TODO: Implement proper v1 ResourceStar system integration return ResponseEntity.ok(0); } else { @@ -295,19 +251,14 @@ public ResponseEntity getComputeResourceStarCount(@PathVariable("id") S @Operation(summary = "Get all starred compute resources") @GetMapping("/starred") - public ResponseEntity> getStarredComputeResources( - @RequestParam(value = "page", defaultValue = "0") int page, - @RequestParam(value = "size", defaultValue = "50") int size) { - LOGGER.info("Fetching starred compute resources - page: {}, size: {}", page, size); + public ResponseEntity> getStarredComputeResources() { + LOGGER.info("Fetching starred compute resources"); try { - Pageable pageable = PageRequest.of(page, size); // TODO: Implement proper v1 ResourceStar system integration - // For now, return empty page - Page starredResources = computeResourceRepository.findByPrivacyAndState(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); - // Filter to empty for now until proper star system is implemented - starredResources = Page.empty(); - LOGGER.info("Found {} starred compute resources", starredResources.getTotalElements()); + // For now, return empty list + List starredResources = List.of(); + LOGGER.info("Found {} starred compute resources", starredResources.size()); return ResponseEntity.ok(starredResources); } catch (Exception e) { LOGGER.error("Error fetching starred compute resources: {}", e.getMessage(), e); diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java index 45d99278e3d..bc38371cd27 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java @@ -23,17 +23,11 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.List; -import java.util.Optional; -import org.apache.airavata.research.service.enums.PrivacyEnum; -import org.apache.airavata.research.service.enums.StateEnum; -import org.apache.airavata.research.service.v2.entity.StorageResource; -import org.apache.airavata.research.service.v2.repository.StorageResourceRepository; +import org.apache.airavata.research.service.dto.StorageResourceDTO; +import org.apache.airavata.research.service.handler.LocalStorageResourceHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindingResult; @@ -53,56 +47,56 @@ public class StorageResourceController { private static final Logger LOGGER = LoggerFactory.getLogger(StorageResourceController.class); - private static final PrivacyEnum PUBLIC_PRIVACY = PrivacyEnum.PUBLIC; - private static final StateEnum ACTIVE_STATE = StateEnum.ACTIVE; - private final StorageResourceRepository storageResourceRepository; + @Autowired + private LocalStorageResourceHandler localStorageResourceHandler; - public StorageResourceController(StorageResourceRepository storageResourceRepository) { - this.storageResourceRepository = storageResourceRepository; - } - - @Operation(summary = "Get all public storage resources with pagination") + @Operation(summary = "Get all public storage resources") @GetMapping("/public") - public ResponseEntity> getStorageResources( - @RequestParam(value = "pageNumber", defaultValue = "0") int pageNumber, - @RequestParam(value = "pageSize", defaultValue = "10") int pageSize, - @RequestParam(value = "nameSearch", required = false) String nameSearch, - @RequestParam(value = "tag", required = false) String[] tags) { - - LOGGER.info("Getting storage resources - page: {}, size: {}, search: {}", pageNumber, pageSize, nameSearch); + public ResponseEntity> getStorageResources( + @RequestParam(value = "nameSearch", required = false) String nameSearch) { - Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending()); - Page resources; + LOGGER.info("Getting storage resources - search: {}", nameSearch); - if (nameSearch != null && !nameSearch.trim().isEmpty()) { - resources = storageResourceRepository.findByNameSearchAndPrivacyAndState(nameSearch, PUBLIC_PRIVACY, ACTIVE_STATE, pageable); - } else { - resources = storageResourceRepository.findByPrivacyAndState(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); + try { + List resources; + + if (nameSearch != null && !nameSearch.trim().isEmpty()) { + resources = localStorageResourceHandler.searchStorageResources(nameSearch); + } else { + resources = localStorageResourceHandler.getAllStorageResources(); + } + + LOGGER.info("Found {} storage resources", resources.size()); + return ResponseEntity.ok(resources); + } catch (Exception e) { + LOGGER.error("Failed to get storage resources: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } - - LOGGER.info("Found {} storage resources", resources.getTotalElements()); - return ResponseEntity.ok(resources); } @Operation(summary = "Get storage resource by ID") @GetMapping("/public/{id}") - public ResponseEntity getStorageResourceById(@PathVariable("id") String id) { + public ResponseEntity getStorageResourceById(@PathVariable("id") String id) { LOGGER.info("Getting storage resource by ID: {}", id); - Optional resource = storageResourceRepository.findById(id); - if (resource.isPresent()) { - return ResponseEntity.ok(resource.get()); - } else { - LOGGER.warn("Storage resource not found with ID: {}", id); - return ResponseEntity.notFound().build(); + try { + StorageResourceDTO resource = localStorageResourceHandler.getStorageResource(id); + return ResponseEntity.ok(resource); + } catch (RuntimeException e) { + if (e.getMessage().contains("not found")) { + LOGGER.warn("Storage resource not found with ID: {}", id); + return ResponseEntity.notFound().build(); + } + LOGGER.error("Error getting storage resource {}: {}", id, e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } @Operation(summary = "Create new storage resource") @PostMapping("/") - public ResponseEntity createStorageResource(@Valid @RequestBody StorageResource storageResource, BindingResult bindingResult) { - LOGGER.info("Creating new storage resource: {}", storageResource.getName()); + public ResponseEntity createStorageResource(@Valid @RequestBody StorageResourceDTO storageResourceDTO, BindingResult bindingResult) { + LOGGER.info("Creating new storage resource: {}", storageResourceDTO.getHostName()); // Validation error handling if (bindingResult.hasErrors()) { @@ -116,25 +110,12 @@ public ResponseEntity createStorageResource(@Valid @RequestBody StorageResour try { // Set default values for fields that might be null - if (storageResource.getCapacityTB() == null) { - storageResource.setCapacityTB(1L); // Default to 1 TB - } - if (storageResource.getSupportsEncryption() == null) { - storageResource.setSupportsEncryption(false); - } - if (storageResource.getSupportsVersioning() == null) { - storageResource.setSupportsVersioning(false); - } - if (storageResource.getPrivacy() == null) { - storageResource.setPrivacy(PUBLIC_PRIVACY); - } - if (storageResource.getState() == null) { - storageResource.setState(ACTIVE_STATE); + if (storageResourceDTO.getCapacityTB() == null) { + storageResourceDTO.setCapacityTB(1L); // Default to 1 TB } - // Note: starCount functionality handled separately in v1 star system - StorageResource savedResource = storageResourceRepository.save(storageResource); - LOGGER.info("Created storage resource with ID: {}", savedResource.getId()); + StorageResourceDTO savedResource = localStorageResourceHandler.createStorageResource(storageResourceDTO); + LOGGER.info("Created storage resource with ID: {}", savedResource.getStorageResourceId()); return ResponseEntity.status(HttpStatus.CREATED).body(savedResource); } catch (Exception e) { @@ -146,7 +127,7 @@ public ResponseEntity createStorageResource(@Valid @RequestBody StorageResour @Operation(summary = "Update storage resource") @PutMapping("/{id}") - public ResponseEntity updateStorageResource(@PathVariable("id") String id, @Valid @RequestBody StorageResource storageResource, BindingResult bindingResult) { + public ResponseEntity updateStorageResource(@PathVariable("id") String id, @Valid @RequestBody StorageResourceDTO storageResourceDTO, BindingResult bindingResult) { LOGGER.info("Updating storage resource with ID: {}", id); // Validation error handling @@ -160,19 +141,7 @@ public ResponseEntity updateStorageResource(@PathVariable("id") String id, @V } try { - Optional existingResource = storageResourceRepository.findById(id); - if (!existingResource.isPresent()) { - LOGGER.warn("Storage resource not found with ID: {}", id); - return ResponseEntity.notFound().build(); - } - - // Set the ID to ensure we update the correct resource - storageResource.setId(id); - - // Preserve creation timestamp - storageResource.setCreatedAt(existingResource.get().getCreatedAt()); - - StorageResource updatedResource = storageResourceRepository.save(storageResource); + StorageResourceDTO updatedResource = localStorageResourceHandler.updateStorageResource(id, storageResourceDTO); LOGGER.info("Successfully updated storage resource with ID: {}", id); return ResponseEntity.ok(updatedResource); @@ -189,13 +158,7 @@ public ResponseEntity deleteStorageResource(@PathVariable("id") String id) { LOGGER.info("Deleting storage resource with ID: {}", id); try { - Optional existingResource = storageResourceRepository.findById(id); - if (!existingResource.isPresent()) { - LOGGER.warn("Storage resource not found with ID: {}", id); - return ResponseEntity.notFound().build(); - } - - storageResourceRepository.deleteById(id); + localStorageResourceHandler.deleteStorageResource(id); LOGGER.info("Successfully deleted storage resource with ID: {}", id); return ResponseEntity.ok().body("Storage resource deleted successfully"); } catch (Exception e) { @@ -207,30 +170,36 @@ public ResponseEntity deleteStorageResource(@PathVariable("id") String id) { @Operation(summary = "Search storage resources by keyword") @GetMapping("/search") - public ResponseEntity> searchStorageResources( + public ResponseEntity> searchStorageResources( @RequestParam(value = "keyword") String keyword) { LOGGER.info("Searching storage resources with keyword: {}", keyword); - List resources = storageResourceRepository - .findByNameContainingIgnoreCaseAndPrivacyAndState(keyword, PUBLIC_PRIVACY, ACTIVE_STATE); - - LOGGER.info("Found {} storage resources matching keyword: {}", resources.size(), keyword); - return ResponseEntity.ok(resources); + try { + List resources = localStorageResourceHandler.searchStorageResources(keyword); + LOGGER.info("Found {} storage resources matching keyword: {}", resources.size(), keyword); + return ResponseEntity.ok(resources); + } catch (Exception e) { + LOGGER.error("Error searching storage resources: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } } @Operation(summary = "Get storage resources by type") @GetMapping("/type/{storageType}") - public ResponseEntity> getStorageResourcesByType( + public ResponseEntity> getStorageResourcesByType( @PathVariable("storageType") String storageType) { LOGGER.info("Getting storage resources by type: {}", storageType); - List resources = storageResourceRepository - .findByStorageTypeAndPrivacyAndState(storageType, PUBLIC_PRIVACY, ACTIVE_STATE); - - LOGGER.info("Found {} storage resources of type: {}", resources.size(), storageType); - return ResponseEntity.ok(resources); + try { + List resources = localStorageResourceHandler.getStorageResourcesByType(storageType); + LOGGER.info("Found {} storage resources of type: {}", resources.size(), storageType); + return ResponseEntity.ok(resources); + } catch (Exception e) { + LOGGER.error("Error filtering storage resources by type: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } } @Operation(summary = "Star/unstar a storage resource") @@ -239,10 +208,7 @@ public ResponseEntity starStorageResource(@PathVariable("id") String id LOGGER.info("Toggling star for storage resource with ID: {}", id); try { - Optional resourceOpt = storageResourceRepository.findById(id); - if (resourceOpt.isPresent()) { - StorageResource resource = resourceOpt.get(); - + if (localStorageResourceHandler.existsStorageResource(id)) { // TODO: Implement proper v1 ResourceStar system integration // For now, return simple toggle response LOGGER.info("Star toggle requested for storage resource: {} (simplified implementation)", id); @@ -263,9 +229,7 @@ public ResponseEntity checkStorageResourceStarred(@PathVariable("id") S LOGGER.info("Checking if storage resource is starred: {}", id); try { - Optional resourceOpt = storageResourceRepository.findById(id); - if (resourceOpt.isPresent()) { - StorageResource resource = resourceOpt.get(); + if (localStorageResourceHandler.existsStorageResource(id)) { // TODO: Implement proper v1 ResourceStar system integration LOGGER.info("Star status check for storage resource: {} (simplified implementation)", id); return ResponseEntity.ok(false); @@ -285,8 +249,7 @@ public ResponseEntity getStorageResourceStarCount(@PathVariable("id") S LOGGER.info("Getting star count for storage resource: {}", id); try { - Optional resourceOpt = storageResourceRepository.findById(id); - if (resourceOpt.isPresent()) { + if (localStorageResourceHandler.existsStorageResource(id)) { // TODO: Implement proper v1 ResourceStar system integration return ResponseEntity.ok(0); } else { @@ -301,19 +264,14 @@ public ResponseEntity getStorageResourceStarCount(@PathVariable("id") S @Operation(summary = "Get all starred storage resources") @GetMapping("/starred") - public ResponseEntity> getStarredStorageResources( - @RequestParam(value = "page", defaultValue = "0") int page, - @RequestParam(value = "size", defaultValue = "50") int size) { - LOGGER.info("Fetching starred storage resources - page: {}, size: {}", page, size); + public ResponseEntity> getStarredStorageResources() { + LOGGER.info("Fetching starred storage resources"); try { - Pageable pageable = PageRequest.of(page, size); // TODO: Implement proper v1 ResourceStar system integration - // For now, return empty page - Page starredResources = storageResourceRepository.findByPrivacyAndState(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); - // Filter to empty for now until proper star system is implemented - starredResources = Page.empty(); - LOGGER.info("Found {} starred storage resources", starredResources.getTotalElements()); + // For now, return empty list + List starredResources = List.of(); + LOGGER.info("Found {} starred storage resources", starredResources.size()); return ResponseEntity.ok(starredResources); } catch (Exception e) { LOGGER.error("Error fetching starred storage resources: {}", e.getMessage(), e); diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java index 9e457e09ece..aa62b425cb7 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java @@ -176,4 +176,40 @@ public String getAdditionalInfo() { public void setAdditionalInfo(String additionalInfo) { this.additionalInfo = additionalInfo; } + + // Additional setter methods for V2DataInitializer compatibility + public void setFileName(String fileName) { + // For models and notebooks, store in notebookPath field + this.notebookPath = fileName; + } + + public String getFileName() { + return this.notebookPath; + } + + public void setBranch(String branch) { + // For repositories, use additionalInfo to store branch info + if (branch != null && !branch.isEmpty()) { + String branchInfo = "Branch: " + branch; + if (this.additionalInfo != null && !this.additionalInfo.isEmpty()) { + this.additionalInfo += "; " + branchInfo; + } else { + this.additionalInfo = branchInfo; + } + } + } + + public String getBranch() { + // Extract branch info from additionalInfo + if (additionalInfo != null && additionalInfo.contains("Branch: ")) { + String[] parts = additionalInfo.split("Branch: "); + if (parts.length > 1) { + String branchPart = parts[1]; + // Get everything before the next semicolon + int semicolonIndex = branchPart.indexOf(";"); + return semicolonIndex > 0 ? branchPart.substring(0, semicolonIndex) : branchPart; + } + } + return null; + } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResourceQueue.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResourceQueue.java deleted file mode 100644 index 737cf07e070..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/ComputeResourceQueue.java +++ /dev/null @@ -1,232 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.FetchType; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import com.fasterxml.jackson.annotation.JsonIgnore; - -@Entity -@Table(name = "COMPUTE_RESOURCE_QUEUE_V2") -public class ComputeResourceQueue { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - @NotBlank(message = "Queue name is required") - @Size(max = 100, message = "Queue name must not exceed 100 characters") - private String queueName; - - @Column(columnDefinition = "TEXT") - private String queueDescription; - - @Column - @Min(value = 1, message = "Queue max run time must be at least 1 minute") - private Integer queueMaxRunTime; // in minutes - - @Column - @Min(value = 1, message = "Queue max nodes must be at least 1") - private Integer queueMaxNodes; - - @Column - @Min(value = 1, message = "Queue max processors must be at least 1") - private Integer queueMaxProcessors; - - @Column - @Min(value = 1, message = "Max jobs in queue must be at least 1") - private Integer maxJobsInQueue; - - @Column - @Min(value = 1, message = "CPUs per node must be at least 1") - private Integer cpusPerNode; - - @Column - @Min(value = 1, message = "Default node count must be at least 1") - private Integer defaultNodeCount; - - @Column - @Min(value = 1, message = "Default CPU count must be at least 1") - private Integer defaultCpuCount; - - @Column - @Min(value = 1, message = "Default wall time must be at least 1 minute") - private Integer defaultWallTime; // in minutes - - @Column(columnDefinition = "TEXT") - private String queueSpecificMacros; - - @Column - private Boolean isDefaultQueue = false; - - // Many-to-one relationship with ComputeResource - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "compute_resource_id", nullable = false) - @JsonIgnore - private ComputeResource computeResource; - - // Default constructor - public ComputeResourceQueue() {} - - // Constructor for creating queue entries - public ComputeResourceQueue(String queueName, String queueDescription, Integer queueMaxRunTime, - Integer queueMaxNodes, Integer queueMaxProcessors, Integer maxJobsInQueue, - Integer cpusPerNode, Integer defaultNodeCount, Integer defaultCpuCount, - Integer defaultWallTime, String queueSpecificMacros, Boolean isDefaultQueue) { - this.queueName = queueName; - this.queueDescription = queueDescription; - this.queueMaxRunTime = queueMaxRunTime; - this.queueMaxNodes = queueMaxNodes; - this.queueMaxProcessors = queueMaxProcessors; - this.maxJobsInQueue = maxJobsInQueue; - this.cpusPerNode = cpusPerNode; - this.defaultNodeCount = defaultNodeCount; - this.defaultCpuCount = defaultCpuCount; - this.defaultWallTime = defaultWallTime; - this.queueSpecificMacros = queueSpecificMacros; - this.isDefaultQueue = isDefaultQueue != null ? isDefaultQueue : false; - } - - // Getters and Setters - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getQueueName() { - return queueName; - } - - public void setQueueName(String queueName) { - this.queueName = queueName; - } - - public String getQueueDescription() { - return queueDescription; - } - - public void setQueueDescription(String queueDescription) { - this.queueDescription = queueDescription; - } - - public Integer getQueueMaxRunTime() { - return queueMaxRunTime; - } - - public void setQueueMaxRunTime(Integer queueMaxRunTime) { - this.queueMaxRunTime = queueMaxRunTime; - } - - public Integer getQueueMaxNodes() { - return queueMaxNodes; - } - - public void setQueueMaxNodes(Integer queueMaxNodes) { - this.queueMaxNodes = queueMaxNodes; - } - - public Integer getQueueMaxProcessors() { - return queueMaxProcessors; - } - - public void setQueueMaxProcessors(Integer queueMaxProcessors) { - this.queueMaxProcessors = queueMaxProcessors; - } - - public Integer getMaxJobsInQueue() { - return maxJobsInQueue; - } - - public void setMaxJobsInQueue(Integer maxJobsInQueue) { - this.maxJobsInQueue = maxJobsInQueue; - } - - public Integer getCpusPerNode() { - return cpusPerNode; - } - - public void setCpusPerNode(Integer cpusPerNode) { - this.cpusPerNode = cpusPerNode; - } - - public Integer getDefaultNodeCount() { - return defaultNodeCount; - } - - public void setDefaultNodeCount(Integer defaultNodeCount) { - this.defaultNodeCount = defaultNodeCount; - } - - public Integer getDefaultCpuCount() { - return defaultCpuCount; - } - - public void setDefaultCpuCount(Integer defaultCpuCount) { - this.defaultCpuCount = defaultCpuCount; - } - - public Integer getDefaultWallTime() { - return defaultWallTime; - } - - public void setDefaultWallTime(Integer defaultWallTime) { - this.defaultWallTime = defaultWallTime; - } - - public String getQueueSpecificMacros() { - return queueSpecificMacros; - } - - public void setQueueSpecificMacros(String queueSpecificMacros) { - this.queueSpecificMacros = queueSpecificMacros; - } - - public Boolean getIsDefaultQueue() { - return isDefaultQueue; - } - - public void setIsDefaultQueue(Boolean isDefaultQueue) { - this.isDefaultQueue = isDefaultQueue; - } - - public ComputeResource getComputeResource() { - return computeResource; - } - - public void setComputeResource(ComputeResource computeResource) { - this.computeResource = computeResource; - } -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java deleted file mode 100644 index 02cc90754bc..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/ComputeResourceRepository.java +++ /dev/null @@ -1,64 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.repository; - -import java.util.List; -import java.util.Optional; -import org.apache.airavata.research.service.v2.entity.ComputeResource; -import org.apache.airavata.research.service.enums.PrivacyEnum; -import org.apache.airavata.research.service.enums.StateEnum; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -@Repository -public interface ComputeResourceRepository extends JpaRepository { - - // Find by name containing (case insensitive) - List findByNameContainingIgnoreCaseAndPrivacyAndState(String name, PrivacyEnum privacy, StateEnum state); - - // Find by compute type - List findByComputeTypeAndPrivacyAndState(String computeType, PrivacyEnum privacy, StateEnum state); - - // Find all public and active resources with pagination - Page findByPrivacyAndState(PrivacyEnum privacy, StateEnum state, Pageable pageable); - - // Search by name with pagination - @Query("SELECT c FROM ComputeResource c WHERE c.privacy = :privacy AND c.state = :state AND " + - "(LOWER(c.name) LIKE LOWER(CONCAT('%', :nameSearch, '%')) OR " + - "LOWER(c.description) LIKE LOWER(CONCAT('%', :nameSearch, '%')) OR " + - "LOWER(c.computeType) LIKE LOWER(CONCAT('%', :nameSearch, '%')))") - Page findByNameSearchAndPrivacyAndState(@Param("nameSearch") String nameSearch, - @Param("privacy") PrivacyEnum privacy, - @Param("state") StateEnum state, - Pageable pageable); - - // Find all public and active resources - List findAllByPrivacyAndState(PrivacyEnum privacy, StateEnum state); - - // Find by ID with eager fetching of queues only - @Query("SELECT DISTINCT c FROM ComputeResource c " + - "LEFT JOIN FETCH c.queues " + - "WHERE c.id = :id") - Optional findByIdWithCollections(@Param("id") String id); -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/StorageResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/StorageResourceRepository.java deleted file mode 100644 index c748d633e3f..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/StorageResourceRepository.java +++ /dev/null @@ -1,57 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.repository; - -import java.util.List; -import org.apache.airavata.research.service.v2.entity.StorageResource; -import org.apache.airavata.research.service.enums.PrivacyEnum; -import org.apache.airavata.research.service.enums.StateEnum; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -@Repository -public interface StorageResourceRepository extends JpaRepository { - - // Find by name containing (case insensitive) - List findByNameContainingIgnoreCaseAndPrivacyAndState(String name, PrivacyEnum privacy, StateEnum state); - - // Find by storage type - List findByStorageTypeAndPrivacyAndState(String storageType, PrivacyEnum privacy, StateEnum state); - - // Find all public and active resources with pagination - Page findByPrivacyAndState(PrivacyEnum privacy, StateEnum state, Pageable pageable); - - // Search by name with pagination - @Query("SELECT s FROM StorageResource s WHERE s.privacy = :privacy AND s.state = :state AND " + - "(LOWER(s.name) LIKE LOWER(CONCAT('%', :nameSearch, '%')) OR " + - "LOWER(s.description) LIKE LOWER(CONCAT('%', :nameSearch, '%')) OR " + - "LOWER(s.storageType) LIKE LOWER(CONCAT('%', :nameSearch, '%')))") - Page findByNameSearchAndPrivacyAndState(@Param("nameSearch") String nameSearch, - @Param("privacy") PrivacyEnum privacy, - @Param("state") StateEnum state, - Pageable pageable); - - // Find all public and active resources - List findAllByPrivacyAndState(PrivacyEnum privacy, StateEnum state); -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/service/ComputeResourceService.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/service/ComputeResourceService.java deleted file mode 100644 index 2f7460e3b6b..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/service/ComputeResourceService.java +++ /dev/null @@ -1,190 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.service; - -import java.util.Optional; -import org.apache.airavata.research.service.v2.entity.ComputeResource; -import org.apache.airavata.research.service.v2.repository.ComputeResourceRepository; -import org.apache.airavata.research.service.enums.PrivacyEnum; -import org.apache.airavata.research.service.enums.StateEnum; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@Service -@Transactional -public class ComputeResourceService { - - private static final Logger LOGGER = LoggerFactory.getLogger(ComputeResourceService.class); - - private final ComputeResourceRepository computeResourceRepository; - - public ComputeResourceService(ComputeResourceRepository computeResourceRepository) { - this.computeResourceRepository = computeResourceRepository; - } - - /** - * Get paginated compute resources with lazy collections properly initialized - */ - @Transactional(readOnly = true) - public Page getComputeResources(PrivacyEnum privacy, StateEnum state, Pageable pageable) { - LOGGER.debug("Fetching compute resources - privacy: {}, state: {}", privacy, state); - - Page resources = computeResourceRepository.findByPrivacyAndState(privacy, state, pageable); - - // Initialize lazy collections within transaction context - resources.getContent().forEach(this::initializeLazyCollections); - - return resources; - } - - /** - * Search compute resources with lazy collections properly initialized - */ - @Transactional(readOnly = true) - public Page searchComputeResources(String nameSearch, PrivacyEnum privacy, StateEnum state, Pageable pageable) { - LOGGER.debug("Searching compute resources - search: {}, privacy: {}, state: {}", nameSearch, privacy, state); - - Page resources = computeResourceRepository.findByNameSearchAndPrivacyAndState(nameSearch, privacy, state, pageable); - - // Initialize lazy collections within transaction context - resources.getContent().forEach(this::initializeLazyCollections); - - return resources; - } - - /** - * Get compute resource by ID with lazy collections properly initialized - */ - @Transactional(readOnly = true) - public Optional getComputeResourceById(String id) { - LOGGER.debug("Fetching compute resource by ID: {}", id); - - Optional resource = computeResourceRepository.findById(id); - - // Initialize lazy collections if resource exists - resource.ifPresent(this::initializeLazyCollections); - - return resource; - } - - /** - * Create new compute resource - */ - public ComputeResource createComputeResource(ComputeResource computeResource) { - LOGGER.debug("Creating compute resource: {}", computeResource.getName()); - - // Set any business logic defaults here if needed - ComputeResource savedResource = computeResourceRepository.save(computeResource); - - // Initialize collections for return - initializeLazyCollections(savedResource); - - return savedResource; - } - - /** - * Update existing compute resource - */ - public Optional updateComputeResource(String id, ComputeResource updatedResource) { - LOGGER.debug("Updating compute resource ID: {}", id); - - Optional existingResource = computeResourceRepository.findById(id); - - if (existingResource.isPresent()) { - ComputeResource resource = existingResource.get(); - - // Update fields - resource.setName(updatedResource.getName()); - resource.setDescription(updatedResource.getDescription()); - resource.setHostname(updatedResource.getHostname()); - resource.setComputeType(updatedResource.getComputeType()); - resource.setCpuCores(updatedResource.getCpuCores()); - resource.setMemoryGB(updatedResource.getMemoryGB()); - resource.setOperatingSystem(updatedResource.getOperatingSystem()); - resource.setQueueSystem(updatedResource.getQueueSystem()); - resource.setResourceManager(updatedResource.getResourceManager()); - resource.setAdditionalInfo(updatedResource.getAdditionalInfo()); - - // Update new fields - resource.setHostAliases(updatedResource.getHostAliases()); - resource.setIpAddresses(updatedResource.getIpAddresses()); - resource.setSshUsername(updatedResource.getSshUsername()); - resource.setSshPort(updatedResource.getSshPort()); - resource.setAuthenticationMethod(updatedResource.getAuthenticationMethod()); - resource.setSshKey(updatedResource.getSshKey()); - resource.setWorkingDirectory(updatedResource.getWorkingDirectory()); - resource.setSchedulerType(updatedResource.getSchedulerType()); - resource.setDataMovementProtocol(updatedResource.getDataMovementProtocol()); - resource.setQueues(updatedResource.getQueues()); - - ComputeResource savedResource = computeResourceRepository.save(resource); - - // Initialize collections for return - initializeLazyCollections(savedResource); - - return Optional.of(savedResource); - } - - return Optional.empty(); - } - - /** - * Delete compute resource - */ - public boolean deleteComputeResource(String id) { - LOGGER.debug("Deleting compute resource ID: {}", id); - - if (computeResourceRepository.existsById(id)) { - computeResourceRepository.deleteById(id); - return true; - } - - return false; - } - - /** - * Initialize lazy collections within transaction context to avoid LazyInitializationException - */ - private void initializeLazyCollections(ComputeResource resource) { - try { - // Force initialization of lazy collections - if (resource.getHostAliases() != null) { - resource.getHostAliases().size(); // Trigger lazy loading - } - if (resource.getIpAddresses() != null) { - resource.getIpAddresses().size(); // Trigger lazy loading - } - if (resource.getQueues() != null) { - resource.getQueues().size(); // Trigger lazy loading - // Also initialize queue details if needed - resource.getQueues().forEach(queue -> { - // Access queue properties to ensure they're loaded - queue.getQueueName(); - }); - } - } catch (Exception e) { - LOGGER.warn("Failed to initialize lazy collections for compute resource {}: {}", resource.getId(), e.getMessage()); - } - } -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/resources/application.yml b/modules/research-framework/research-service/src/main/resources/application.yml index cb803755096..bb6923287eb 100644 --- a/modules/research-framework/research-service/src/main/resources/application.yml +++ b/modules/research-framework/research-service/src/main/resources/application.yml @@ -37,6 +37,10 @@ airavata: server: url: airavata.host port: 8962 + registry: + host: localhost + port: 9930 + enabled: false # Disabled for development - using direct database access spring: servlet: From ae33230fcf1895d6097c85eaacb22764adc1994b Mon Sep 17 00:00:00 2001 From: Krish Katariya Date: Thu, 31 Jul 2025 21:32:51 -0700 Subject: [PATCH 09/17] Bug fixes --- .../service/model/entity/StorageResource.java | 60 ------------------- .../model/repo/StorageResourceRepository.java | 1 - 2 files changed, 61 deletions(-) delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/StorageResource.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/StorageResourceRepository.java diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/StorageResource.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/StorageResource.java deleted file mode 100644 index ab37177060d..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/StorageResource.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.apache.airavata.research.service.model.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; - -@Entity -@Table(name = "storage_resources") -public class StorageResource { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private String name; - - @Column(nullable = false) - private String storage; - - @Column(name = "storage_type", nullable = false) - private String storageType; - - @Column(nullable = false) - private String status; - - private String description; - - // Constructors - public StorageResource() {} - - public StorageResource(String name, String storage, String storageType, String status, String description) { - this.name = name; - this.storage = storage; - this.storageType = storageType; - this.status = status; - this.description = description; - } - - // Getters and setters - public Long getId() { return id; } - public void setId(Long id) { this.id = id; } - - public String getName() { return name; } - public void setName(String name) { this.name = name; } - - public String getStorage() { return storage; } - public void setStorage(String storage) { this.storage = storage; } - - public String getStorageType() { return storageType; } - public void setStorageType(String storageType) { this.storageType = storageType; } - - public String getStatus() { return status; } - public void setStatus(String status) { this.status = status; } - - public String getDescription() { return description; } - public void setDescription(String description) { this.description = description; } -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/StorageResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/StorageResourceRepository.java deleted file mode 100644 index 0519ecba6ea..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/StorageResourceRepository.java +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 83101a04514b2c7008589cd1cd4f14b8b2023161 Mon Sep 17 00:00:00 2001 From: Krish Katariya Date: Fri, 1 Aug 2025 19:13:36 -0700 Subject: [PATCH 10/17] Replicating airavata-api entities & using app catalog to store data --- .../service/ResearchServiceApplication.java | 2 - .../config/AppCatalogDatabaseConfig.java | 91 +++++ .../CustomEntityManagerFactoryBean.java | 81 +++++ .../service/config/DatasetInitializer.java | 211 +++++++++++ .../service/config/DevDataInitializer.java | 2 +- .../config/LocalResourceDataInitializer.java | 278 -------------- .../service/config/ProjectDatabaseConfig.java | 84 +++++ .../service/entity/ComputeResourceEntity.java | 166 +++++++++ .../entity/LocalComputeResourceEntity.java | 179 --------- .../entity/LocalStorageResourceEntity.java | 124 ------- .../service/entity/StorageResourceEntity.java | 78 ++++ .../handler/ComputeResourceHandler.java | 297 +++++++-------- .../handler/LocalComputeResourceHandler.java | 230 ------------ .../handler/LocalStorageResourceHandler.java | 257 ------------- .../handler/StorageResourceHandler.java | 340 ++++++++---------- .../research/service/model/entity/Tag.java | 3 + .../repository/ComputeResourceRepository.java | 37 ++ .../LocalComputeResourceRepository.java | 55 --- .../LocalStorageResourceRepository.java | 55 --- .../repository/StorageResourceRepository.java | 37 ++ .../research/service/util/DTOConverter.java | 54 +-- .../controller/ComputeResourceController.java | 24 +- .../controller/StorageResourceController.java | 26 +- .../src/main/resources/application.yml | 14 +- 24 files changed, 1134 insertions(+), 1591 deletions(-) create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AppCatalogDatabaseConfig.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/CustomEntityManagerFactoryBean.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DatasetInitializer.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/LocalResourceDataInitializer.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/ProjectDatabaseConfig.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/ComputeResourceEntity.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/LocalComputeResourceEntity.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/LocalStorageResourceEntity.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/StorageResourceEntity.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/LocalComputeResourceHandler.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/LocalStorageResourceHandler.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/ComputeResourceRepository.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/LocalComputeResourceRepository.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/LocalStorageResourceRepository.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/StorageResourceRepository.java diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/ResearchServiceApplication.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/ResearchServiceApplication.java index c24f8e75c63..adf3686948b 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/ResearchServiceApplication.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/ResearchServiceApplication.java @@ -22,10 +22,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @SpringBootApplication -@EnableJpaRepositories() @EnableJpaAuditing public class ResearchServiceApplication { public static void main(String[] args) { diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AppCatalogDatabaseConfig.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AppCatalogDatabaseConfig.java new file mode 100644 index 00000000000..fb9cfcbb1bf --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AppCatalogDatabaseConfig.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.airavata.research.service.config; + +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; + +import javax.sql.DataSource; +import java.util.Properties; + +@Configuration +@EnableJpaRepositories( + basePackages = "org.apache.airavata.research.service.repository", + entityManagerFactoryRef = "appCatalogEntityManagerFactory", + transactionManagerRef = "appCatalogTransactionManager" +) +public class AppCatalogDatabaseConfig { + + @Bean + @ConfigurationProperties("app.datasource.app-catalog") + public DataSourceProperties appCatalogDataSourceProperties() { + return new DataSourceProperties(); + } + + @Bean + public DataSource appCatalogDataSource() { + return appCatalogDataSourceProperties() + .initializeDataSourceBuilder() + .build(); + } + + @Bean + public LocalContainerEntityManagerFactoryBean appCatalogEntityManagerFactory() { + LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); + em.setDataSource(appCatalogDataSource()); + em.setPersistenceUnitName("appCatalogPU"); + + // Scan our local entities that mirror database schema exactly + em.setPackagesToScan("org.apache.airavata.research.service.entity"); + + HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + vendorAdapter.setGenerateDdl(false); // Don't modify existing schema + vendorAdapter.setShowSql(false); + em.setJpaVendorAdapter(vendorAdapter); + + Properties props = new Properties(); + props.setProperty("hibernate.hbm2ddl.auto", "none"); // Don't modify existing schema + props.setProperty("hibernate.dialect", "org.hibernate.dialect.MariaDBDialect"); + props.setProperty("hibernate.format_sql", "true"); + props.setProperty("hibernate.show_sql", "false"); + + // Disable validation to avoid issues with existing data + props.setProperty("hibernate.validator.apply_to_ddl", "false"); + props.setProperty("hibernate.check_nullability", "false"); + props.setProperty("jakarta.persistence.validation.mode", "NONE"); + + em.setJpaProperties(props); + + return em; + } + + @Bean + public PlatformTransactionManager appCatalogTransactionManager() { + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(appCatalogEntityManagerFactory().getObject()); + return transactionManager; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/CustomEntityManagerFactoryBean.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/CustomEntityManagerFactoryBean.java new file mode 100644 index 00000000000..df4048c39e8 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/CustomEntityManagerFactoryBean.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.airavata.research.service.config; + +import org.apache.airavata.registry.core.entities.appcatalog.ComputeResourceEntity; +import org.apache.airavata.registry.core.entities.appcatalog.StorageResourceEntity; +import org.hibernate.boot.Metadata; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; + +import javax.sql.DataSource; +import jakarta.persistence.EntityManagerFactory; +import java.util.Properties; + +/** + * Custom EntityManagerFactory that programmatically registers ONLY the specific + * entities we need, completely bypassing package scanning to avoid ParserInputEntity + * and other problematic entities. + */ +public class CustomEntityManagerFactoryBean extends LocalContainerEntityManagerFactoryBean { + + private final DataSource dataSource; + private final Properties hibernateProperties; + + public CustomEntityManagerFactoryBean(DataSource dataSource, Properties hibernateProperties) { + this.dataSource = dataSource; + this.hibernateProperties = hibernateProperties; + setDataSource(dataSource); + setPersistenceUnitName("appCatalogPU"); + // DO NOT set packages to scan - we will register entities manually + } + + @Override + protected EntityManagerFactory createNativeEntityManagerFactory() { + // Create Hibernate service registry with our properties + StandardServiceRegistryBuilder registryBuilder = new StandardServiceRegistryBuilder(); + + // Add datasource configuration + hibernateProperties.put("hibernate.connection.datasource", dataSource); + registryBuilder.applySettings(hibernateProperties); + + StandardServiceRegistry registry = registryBuilder.build(); + + try { + // Create metadata sources and add ONLY our specific entities + MetadataSources metadataSources = new MetadataSources(registry); + + // CRITICAL: Add ONLY the entities we need - no package scanning + metadataSources.addAnnotatedClass(ComputeResourceEntity.class); + metadataSources.addAnnotatedClass(StorageResourceEntity.class); + + // Build metadata and create session factory + Metadata metadata = metadataSources.buildMetadata(); + + // Return the EntityManagerFactory from the session factory + return metadata.buildSessionFactory().unwrap(EntityManagerFactory.class); + + } catch (Exception e) { + StandardServiceRegistryBuilder.destroy(registry); + throw new RuntimeException("Failed to create EntityManagerFactory", e); + } + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DatasetInitializer.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DatasetInitializer.java new file mode 100644 index 00000000000..f120e8e17e1 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DatasetInitializer.java @@ -0,0 +1,211 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.config; + +import jakarta.annotation.PostConstruct; +import java.util.HashSet; +import java.util.Set; +import org.apache.airavata.research.service.enums.PrivacyEnum; +import org.apache.airavata.research.service.enums.StateEnum; +import org.apache.airavata.research.service.enums.StatusEnum; +import org.apache.airavata.research.service.model.entity.DatasetResource; +import org.apache.airavata.research.service.model.entity.Tag; +import org.apache.airavata.research.service.model.repo.ResourceRepository; +import org.apache.airavata.research.service.model.repo.TagRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * Dataset Initializer for creating sample dataset resources + * Runs automatically without requiring dev-local profile + */ +@Component +public class DatasetInitializer { + + private static final Logger LOGGER = LoggerFactory.getLogger(DatasetInitializer.class); + + private final ResourceRepository resourceRepository; + private final TagRepository tagRepository; + + public DatasetInitializer(ResourceRepository resourceRepository, TagRepository tagRepository) { + this.resourceRepository = resourceRepository; + this.tagRepository = tagRepository; + } + + @PostConstruct + public void initializeDatasets() { + LOGGER.info("Initializing dataset resources..."); + + try { + // Only initialize if no datasets exist + long datasetCount = resourceRepository.findAll().stream() + .filter(resource -> resource instanceof DatasetResource) + .count(); + + if (datasetCount == 0) { + LOGGER.info("Creating sample dataset resources..."); + createSampleDatasets(); + LOGGER.info("Dataset initialization completed."); + } else { + LOGGER.info("Datasets already exist. Skipping initialization."); + } + } catch (Exception e) { + LOGGER.error("Error during dataset initialization: {}", e.getMessage(), e); + throw new RuntimeException("Failed to initialize dataset resources", e); + } + } + + private void createSampleDatasets() { + // Create diverse sample datasets + DatasetData[] datasetArray = { + new DatasetData( + "Lung CT Scans Database", + "A comprehensive collection of 3D lung CT images for medical imaging research and machine learning model training.", + "lung-ct-scans-db", + Set.of("medical@imaging.lab", "ssaggi3@gatech.edu"), + Set.of("medical", "ct-scans", "lungs", "imaging") + ), + + new DatasetData( + "Financial Fraud Dataset", + "Large-scale dataset of credit card transactions with fraud labels for developing fraud detection systems.", + "financial-fraud-dataset", + Set.of("fraud@detection.org", "security@fintech.com"), + Set.of("finance", "fraud", "transactions", "cybersecurity") + ), + + new DatasetData( + "Stock Market Data", + "Historical stock prices and trading volumes for major market indices over the past 20 years.", + "stock-market-historical-data", + Set.of("market@data.finance", "trading@analytics.com"), + Set.of("finance", "stocks", "time-series", "trading") + ), + + new DatasetData( + "Drug Compound Library", + "Chemical compound structures and properties for pharmaceutical research and drug discovery.", + "drug-compound-library", + Set.of("pharma@research.edu", "compounds@drugdev.org"), + Set.of("healthcare", "compounds", "pharmaceuticals", "chemistry") + ), + + new DatasetData( + "Social Media Sentiment", + "Annotated social media posts with sentiment labels for natural language processing and sentiment analysis.", + "social-media-sentiment-dataset", + Set.of("nlp@sentiment.lab", "text@analysis.ai"), + Set.of("nlp", "sentiment", "social-media", "text") + ), + + new DatasetData( + "Protein Sequences", + "Large collection of protein sequences with structural annotations for bioinformatics research.", + "protein-sequences-annotated", + Set.of("bio@sequences.org", "proteins@research.edu"), + Set.of("life-sciences", "proteins", "sequences", "bioinformatics") + ), + + new DatasetData( + "ImageNet Subset", + "Curated subset of ImageNet for computer vision benchmarking and deep learning model evaluation.", + "imagenet-curated-subset", + Set.of("vision@datasets.org", "images@cv.lab"), + Set.of("computer-vision", "images", "classification", "benchmark") + ), + + new DatasetData( + "Malware Samples", + "Classified malware samples for cybersecurity research and threat detection system training.", + "malware-samples-classified", + Set.of("security@malware.lab", "threats@cyber.defense"), + Set.of("cybersecurity", "malware", "security", "threats") + ), + + new DatasetData( + "Climate Data Collection", + "Long-term climate measurements including temperature, precipitation, and atmospheric data.", + "climate-data-collection", + Set.of("climate@research.org", "weather@prediction.lab"), + Set.of("climate", "environmental", "weather", "data-analysis") + ) + }; + + // Create datasets from sample data + for (DatasetData datasetData : datasetArray) { + DatasetResource dataset = createDatasetFromData(datasetData); + resourceRepository.save(dataset); + } + + LOGGER.info("Created {} dataset resources", datasetArray.length); + } + + private DatasetResource createDatasetFromData(DatasetData data) { + DatasetResource dataset = new DatasetResource(); + dataset.setName(data.name); + dataset.setDescription(data.description); + dataset.setDatasetUrl(data.datasetUrl); + + // Set default Resource fields (inherited) + dataset.setPrivacy(PrivacyEnum.PUBLIC); + dataset.setState(StateEnum.ACTIVE); + dataset.setStatus(StatusEnum.VERIFIED); + dataset.setAuthors(new HashSet<>(data.authors)); + dataset.setTags(getOrCreateTags(data.tags)); + dataset.setHeaderImage(""); // Default empty header image + + return dataset; + } + + private Set getOrCreateTags(Set tagNames) { + Set tags = new HashSet<>(); + for (String tagName : tagNames) { + Tag existingTag = tagRepository.findByValue(tagName); + if (existingTag != null) { + tags.add(existingTag); + } else { + Tag newTag = new Tag(); + newTag.setValue(tagName); + Tag savedTag = tagRepository.save(newTag); + tags.add(savedTag); + } + } + return tags; + } + + // Helper class for organizing sample data + private static class DatasetData { + final String name; + final String description; + final String datasetUrl; + final Set authors; + final Set tags; + + public DatasetData(String name, String description, String datasetUrl, + Set authors, Set tags) { + this.name = name; + this.description = description; + this.datasetUrl = datasetUrl; + this.authors = authors; + this.tags = tags; + } + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java index ceb321fbbf7..55a5a4d4ee5 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java @@ -36,7 +36,7 @@ import org.springframework.stereotype.Component; @Component -@Profile("dev") +@Profile("dev-local") public class DevDataInitializer implements CommandLineRunner { private final ProjectRepository projectRepository; diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/LocalResourceDataInitializer.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/LocalResourceDataInitializer.java deleted file mode 100644 index f9a1d8f8f26..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/LocalResourceDataInitializer.java +++ /dev/null @@ -1,278 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.config; - -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.annotation.PostConstruct; -import java.sql.Timestamp; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import org.apache.airavata.research.service.entity.LocalComputeResourceEntity; -import org.apache.airavata.research.service.entity.LocalStorageResourceEntity; -import org.apache.airavata.research.service.repository.LocalComputeResourceRepository; -import org.apache.airavata.research.service.repository.LocalStorageResourceRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -/** - * Local data initializer using existing airavata-api entities - * Generates sample data for development without external registry services - */ -@Component -public class LocalResourceDataInitializer { - - private static final Logger LOGGER = LoggerFactory.getLogger(LocalResourceDataInitializer.class); - private final ObjectMapper objectMapper = new ObjectMapper(); - - private final LocalStorageResourceRepository storageResourceRepository; - private final LocalComputeResourceRepository computeResourceRepository; - - public LocalResourceDataInitializer(LocalStorageResourceRepository storageResourceRepository, - LocalComputeResourceRepository computeResourceRepository) { - this.storageResourceRepository = storageResourceRepository; - this.computeResourceRepository = computeResourceRepository; - } - - @PostConstruct - public void initializeData() { - LOGGER.info("Initializing local resource data using airavata-api entities..."); - - try { - initializeStorageResources(); - initializeComputeResources(); - - LOGGER.info("Local resource data initialization completed."); - } catch (Exception e) { - LOGGER.error("Error during local resource data initialization: {}", e.getMessage(), e); - throw new RuntimeException("Failed to initialize local resource data", e); - } - } - - private void initializeStorageResources() { - if (storageResourceRepository.count() == 0) { - LOGGER.info("Creating local storage resources using LocalStorageResourceEntity..."); - - LocalStorageResourceEntity[] storageResources = { - createS3StorageResource( - "HPC Research Data Lake", - "s3.research.university.edu", - "Large-scale S3-compatible storage for computational research data with 500TB capacity", - createS3UIFields("hpc-research-data-lake", "us-east-1", 500L) - ), - - createS3StorageResource( - "Genomics Cloud Storage", - "genomics-s3.cloud.edu", - "Specialized S3 storage for genomics research data with HIPAA compliance", - createS3UIFields("genomics-research-bucket", "us-west-2", 1000L) - ), - - createS3StorageResource( - "Neural Network Model Archive", - "ml-models.storage.edu", - "S3 storage optimized for deep learning model artifacts and training datasets", - createS3UIFields("neural-network-models", "us-east-1", 250L) - ), - - createSCPStorageResource( - "Supercomputer Scratch Storage", - "hpc-cluster.university.edu", - "High-performance parallel filesystem on supercomputing cluster for active computations", - createSCPUIFields(22, "research_user", "/scratch/research_projects", 2000L) - ), - - createSCPStorageResource( - "Lab Server Archive", - "lab-server.research.university.edu", - "Local lab server storage for secure research data archival and backup", - createSCPUIFields(2222, "lab_admin", "/data/archive", 50L) - ), - - createSCPStorageResource( - "Collaboration Storage Server", - "collab-storage.consortium.org", - "Shared storage server for multi-institutional research collaboration", - createSCPUIFields(22, "collab_user", "/shared/projects", 100L) - ) - }; - - for (LocalStorageResourceEntity storage : storageResources) { - storageResourceRepository.save(storage); - } - - LOGGER.info("Created {} local storage resources", storageResources.length); - } - } - - private void initializeComputeResources() { - if (computeResourceRepository.count() == 0) { - LOGGER.info("Creating local compute resources using LocalComputeResourceEntity..."); - - LocalComputeResourceEntity[] computeResources = { - createComputeResource( - "Anvil Supercomputer (CPU)", - "anvil.rcac.purdue.edu", - "NSF-funded supercomputer at Purdue University with CPU nodes for large-scale scientific computing", - 128, 4, 256, 30, - createComputeUIFields("ANVIL_CPU", "CentOS_7", "SLURM", "SCP") - ), - - createComputeResource( - "Anvil Supercomputer (GPU)", - "anvil-gpu.rcac.purdue.edu", - "NSF-funded supercomputer at Purdue University with GPU nodes for AI/ML workloads", - 128, 4, 256, 30, - createComputeUIFields("ANVIL_GPU", "CentOS_7", "SLURM", "SCP") - ), - - createComputeResource( - "Bridges-2 (PSC)", - "bridges2.psc.edu", - "NSF-funded supercomputer at Pittsburgh Supercomputing Center for diverse research workloads", - 128, 8, 512, 48, - createComputeUIFields("BRIDGES2", "CentOS_7", "SLURM", "SCP") - ), - - createComputeResource( - "Expanse (SDSC)", - "login.expanse.sdsc.edu", - "NSF-funded supercomputer at San Diego Supercomputer Center for computational research", - 128, 2, 256, 48, - createComputeUIFields("EXPANSE", "CentOS_8", "SLURM", "SCP") - ), - - createComputeResource( - "Delta GPU Cluster", - "login.delta.ncsa.illinois.edu", - "GPU-focused supercomputer at NCSA for AI, machine learning, and data science workloads", - 64, 8, 256, 30, - createComputeUIFields("DELTA_GPU", "Red_Hat_8", "SLURM", "SCP") - ), - - createComputeResource( - "Stampede3 (TACC)", - "stampede3.tacc.utexas.edu", - "Advanced supercomputer at Texas Advanced Computing Center for high-performance computing", - 96, 4, 192, 48, - createComputeUIFields("STAMPEDE3", "CentOS_7", "SLURM", "SCP") - ) - }; - - for (LocalComputeResourceEntity compute : computeResources) { - computeResourceRepository.save(compute); - } - - LOGGER.info("Created {} local compute resources", computeResources.length); - } - } - - private LocalStorageResourceEntity createS3StorageResource(String description, String hostName, - String fullDescription, String uiFieldsJson) { - LocalStorageResourceEntity storage = new LocalStorageResourceEntity(); - storage.setStorageResourceId(UUID.randomUUID().toString()); - storage.setStorageResourceDescription(fullDescription + "\n\nUI_FIELDS: " + uiFieldsJson); - storage.setHostName(hostName); - storage.setEnabled(true); - storage.setCreationTime(new Timestamp(System.currentTimeMillis())); - storage.setUpdateTime(new Timestamp(System.currentTimeMillis())); - return storage; - } - - private LocalStorageResourceEntity createSCPStorageResource(String description, String hostName, - String fullDescription, String uiFieldsJson) { - LocalStorageResourceEntity storage = new LocalStorageResourceEntity(); - storage.setStorageResourceId(UUID.randomUUID().toString()); - storage.setStorageResourceDescription(fullDescription + "\n\nUI_FIELDS: " + uiFieldsJson); - storage.setHostName(hostName); - storage.setEnabled(true); - storage.setCreationTime(new Timestamp(System.currentTimeMillis())); - storage.setUpdateTime(new Timestamp(System.currentTimeMillis())); - return storage; - } - - private LocalComputeResourceEntity createComputeResource(String description, String hostName, String fullDescription, - int cpusPerNode, int defaultNodeCount, int maxMemoryPerNode, - int defaultWalltime, String uiFieldsJson) { - LocalComputeResourceEntity compute = new LocalComputeResourceEntity(); - compute.setComputeResourceId(UUID.randomUUID().toString()); - compute.setResourceDescription(fullDescription + "\n\nUI_FIELDS: " + uiFieldsJson); - compute.setHostName(hostName); - compute.setEnabled((short) 1); - compute.setCpusPerNode(cpusPerNode); - compute.setDefaultNodeCount(defaultNodeCount); - compute.setDefaultCPUCount(cpusPerNode * defaultNodeCount); - compute.setMaxMemoryPerNode(maxMemoryPerNode); - compute.setDefaultWalltime(defaultWalltime); - compute.setCreationTime(new Timestamp(System.currentTimeMillis())); - compute.setUpdateTime(new Timestamp(System.currentTimeMillis())); - return compute; - } - - // Helper methods to create UI-specific field JSON - private String createS3UIFields(String bucketName, String region, Long capacityTB) { - try { - Map uiFields = new HashMap<>(); - uiFields.put("storageType", "S3"); - uiFields.put("accessProtocol", "S3"); - uiFields.put("bucketName", bucketName); - uiFields.put("region", region); - uiFields.put("capacityTB", capacityTB); - uiFields.put("supportsEncryption", true); - uiFields.put("supportsVersioning", true); - return objectMapper.writeValueAsString(uiFields); - } catch (Exception e) { - LOGGER.warn("Failed to serialize S3 UI fields", e); - return "{}"; - } - } - - private String createSCPUIFields(Integer port, String username, String remotePath, Long capacityTB) { - try { - Map uiFields = new HashMap<>(); - uiFields.put("storageType", "SCP"); - uiFields.put("accessProtocol", "SCP"); - uiFields.put("port", port); - uiFields.put("username", username); - uiFields.put("remotePath", remotePath); - uiFields.put("capacityTB", capacityTB); - uiFields.put("authenticationMethod", "SSH_KEY"); - return objectMapper.writeValueAsString(uiFields); - } catch (Exception e) { - LOGGER.warn("Failed to serialize SCP UI fields", e); - return "{}"; - } - } - - private String createComputeUIFields(String computeType, String operatingSystem, String schedulerType, String dataMovementProtocol) { - try { - Map uiFields = new HashMap<>(); - uiFields.put("computeType", computeType); - uiFields.put("operatingSystem", operatingSystem); - uiFields.put("schedulerType", schedulerType); - uiFields.put("dataMovementProtocol", dataMovementProtocol); - return objectMapper.writeValueAsString(uiFields); - } catch (Exception e) { - LOGGER.warn("Failed to serialize compute UI fields", e); - return "{}"; - } - } -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/ProjectDatabaseConfig.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/ProjectDatabaseConfig.java new file mode 100644 index 00000000000..78c59607550 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/ProjectDatabaseConfig.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.airavata.research.service.config; + +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; + +import javax.sql.DataSource; +import java.util.Properties; + +@Configuration +@EnableJpaRepositories( + basePackages = {"org.apache.airavata.research.service.model.repo", "org.apache.airavata.research.service.v2.repository"}, + entityManagerFactoryRef = "entityManagerFactory", + transactionManagerRef = "transactionManager" +) +public class ProjectDatabaseConfig { + + @Bean + @ConfigurationProperties("spring.datasource") + public DataSourceProperties projectDataSourceProperties() { + return new DataSourceProperties(); + } + + @Bean + public DataSource dataSource() { + return projectDataSourceProperties() + .initializeDataSourceBuilder() + .build(); + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory() { + LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); + em.setDataSource(dataSource()); + em.setPersistenceUnitName("projectPU"); + em.setPackagesToScan("org.apache.airavata.research.service.model.entity", "org.apache.airavata.research.service.v2.entity"); + + HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + vendorAdapter.setGenerateDdl(true); + vendorAdapter.setShowSql(true); + em.setJpaVendorAdapter(vendorAdapter); + + Properties jpaProperties = new Properties(); + jpaProperties.put("hibernate.hbm2ddl.auto", "update"); + jpaProperties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect"); + jpaProperties.put("hibernate.show_sql", true); + jpaProperties.put("hibernate.format_sql", true); + + em.setJpaProperties(jpaProperties); + + return em; + } + + @Bean + public PlatformTransactionManager transactionManager() { + JpaTransactionManager transactionManager = new JpaTransactionManager(); + transactionManager.setEntityManagerFactory(entityManagerFactory().getObject()); + return transactionManager; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/ComputeResourceEntity.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/ComputeResourceEntity.java new file mode 100644 index 00000000000..8fa1a0f84d5 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/ComputeResourceEntity.java @@ -0,0 +1,166 @@ +package org.apache.airavata.research.service.entity; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.sql.Timestamp; + +@Entity +@Table(name = "COMPUTE_RESOURCE") +public class ComputeResourceEntity implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "RESOURCE_ID") + private String resourceId; + + @Column(name = "HOST_NAME", nullable = false) + private String hostName; + + @Column(name = "RESOURCE_DESCRIPTION") + private String resourceDescription; + + @Column(name = "CREATION_TIME", nullable = false) + private Timestamp creationTime; + + @Column(name = "UPDATE_TIME", nullable = false) + private Timestamp updateTime; + + @Column(name = "MAX_MEMORY_NODE") + private Integer maxMemoryNode; + + @Column(name = "CPUS_PER_NODE") + private Integer cpusPerNode; + + @Column(name = "DEFAULT_NODE_COUNT") + private Integer defaultNodeCount; + + @Column(name = "DEFAULT_CPU_COUNT") + private Integer defaultCpuCount; + + @Column(name = "DEFAULT_WALLTIME") + private Integer defaultWalltime; + + @Column(name = "ENABLED") + private Short enabled; + + @Column(name = "GATEWAY_USAGE_REPORTING") + private Boolean gatewayUsageReporting; + + @Column(name = "GATEWAY_USAGE_MODULE_LOAD_CMD", length = 500) + private String gatewayUsageModuleLoadCmd; + + @Column(name = "GATEWAY_USAGE_EXECUTABLE") + private String gatewayUsageExecutable; + + public String getResourceId() { + return resourceId; + } + + public void setResourceId(String resourceId) { + this.resourceId = resourceId; + } + + public String getHostName() { + return hostName; + } + + public void setHostName(String hostName) { + this.hostName = hostName; + } + + public String getResourceDescription() { + return resourceDescription; + } + + public void setResourceDescription(String resourceDescription) { + this.resourceDescription = resourceDescription; + } + + public Timestamp getCreationTime() { + return creationTime; + } + + public void setCreationTime(Timestamp creationTime) { + this.creationTime = creationTime; + } + + public Timestamp getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Timestamp updateTime) { + this.updateTime = updateTime; + } + + public Integer getMaxMemoryNode() { + return maxMemoryNode; + } + + public void setMaxMemoryNode(Integer maxMemoryNode) { + this.maxMemoryNode = maxMemoryNode; + } + + public Integer getCpusPerNode() { + return cpusPerNode; + } + + public void setCpusPerNode(Integer cpusPerNode) { + this.cpusPerNode = cpusPerNode; + } + + public Integer getDefaultNodeCount() { + return defaultNodeCount; + } + + public void setDefaultNodeCount(Integer defaultNodeCount) { + this.defaultNodeCount = defaultNodeCount; + } + + public Integer getDefaultCpuCount() { + return defaultCpuCount; + } + + public void setDefaultCpuCount(Integer defaultCpuCount) { + this.defaultCpuCount = defaultCpuCount; + } + + public Integer getDefaultWalltime() { + return defaultWalltime; + } + + public void setDefaultWalltime(Integer defaultWalltime) { + this.defaultWalltime = defaultWalltime; + } + + public Short getEnabled() { + return enabled; + } + + public void setEnabled(Short enabled) { + this.enabled = enabled; + } + + public Boolean getGatewayUsageReporting() { + return gatewayUsageReporting; + } + + public void setGatewayUsageReporting(Boolean gatewayUsageReporting) { + this.gatewayUsageReporting = gatewayUsageReporting; + } + + public String getGatewayUsageModuleLoadCmd() { + return gatewayUsageModuleLoadCmd; + } + + public void setGatewayUsageModuleLoadCmd(String gatewayUsageModuleLoadCmd) { + this.gatewayUsageModuleLoadCmd = gatewayUsageModuleLoadCmd; + } + + public String getGatewayUsageExecutable() { + return gatewayUsageExecutable; + } + + public void setGatewayUsageExecutable(String gatewayUsageExecutable) { + this.gatewayUsageExecutable = gatewayUsageExecutable; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/LocalComputeResourceEntity.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/LocalComputeResourceEntity.java deleted file mode 100644 index 4584ca077f4..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/LocalComputeResourceEntity.java +++ /dev/null @@ -1,179 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.entity; - -import jakarta.persistence.*; -import java.io.Serializable; -import java.sql.Timestamp; - -/** - * Local entity that mirrors airavata-api ComputeResourceEntity structure - * Used for local development without external airavata-api dependencies - */ -@Entity -@Table(name = "LOCAL_COMPUTE_RESOURCE") -public class LocalComputeResourceEntity implements Serializable { - private static final long serialVersionUID = 1L; - - @Id - @Column(name = "RESOURCE_ID") - private String computeResourceId; - - @Column(name = "CREATION_TIME") - private Timestamp creationTime; - - @Column(name = "ENABLED") - private short enabled; - - @Column(name = "HOST_NAME") - private String hostName; - - @Column(name = "MAX_MEMORY_NODE") - private int maxMemoryPerNode; - - @Column(name = "RESOURCE_DESCRIPTION", length = 4000) - private String resourceDescription; - - @Column(name = "UPDATE_TIME") - private Timestamp updateTime; - - @Column(name = "CPUS_PER_NODE") - private Integer cpusPerNode; - - @Column(name = "DEFAULT_NODE_COUNT") - private Integer defaultNodeCount; - - @Column(name = "DEFAULT_CPU_COUNT") - private Integer defaultCPUCount; - - @Column(name = "DEFAULT_WALLTIME") - private Integer defaultWalltime; - - // Default constructor - public LocalComputeResourceEntity() {} - - // Constructor matching airavata-api pattern - public LocalComputeResourceEntity(String computeResourceId, String hostName, String description) { - this.computeResourceId = computeResourceId; - this.hostName = hostName; - this.resourceDescription = description; - this.enabled = 1; - this.creationTime = new Timestamp(System.currentTimeMillis()); - this.updateTime = new Timestamp(System.currentTimeMillis()); - } - - // Getters and setters (matching airavata-api naming) - public String getComputeResourceId() { - return computeResourceId; - } - - public void setComputeResourceId(String computeResourceId) { - this.computeResourceId = computeResourceId; - } - - public Timestamp getCreationTime() { - return creationTime; - } - - public void setCreationTime(Timestamp creationTime) { - this.creationTime = creationTime; - } - - public short getEnabled() { - return enabled; - } - - public void setEnabled(short enabled) { - this.enabled = enabled; - } - - public String getHostName() { - return hostName; - } - - public void setHostName(String hostName) { - this.hostName = hostName; - } - - public int getMaxMemoryPerNode() { - return maxMemoryPerNode; - } - - public void setMaxMemoryPerNode(int maxMemoryPerNode) { - this.maxMemoryPerNode = maxMemoryPerNode; - } - - public String getResourceDescription() { - return resourceDescription; - } - - public void setResourceDescription(String resourceDescription) { - this.resourceDescription = resourceDescription; - } - - public Timestamp getUpdateTime() { - return updateTime; - } - - public void setUpdateTime(Timestamp updateTime) { - this.updateTime = updateTime; - } - - public Integer getCpusPerNode() { - return cpusPerNode; - } - - public void setCpusPerNode(Integer cpusPerNode) { - this.cpusPerNode = cpusPerNode; - } - - public Integer getDefaultNodeCount() { - return defaultNodeCount; - } - - public void setDefaultNodeCount(Integer defaultNodeCount) { - this.defaultNodeCount = defaultNodeCount; - } - - public Integer getDefaultCPUCount() { - return defaultCPUCount; - } - - public void setDefaultCPUCount(Integer defaultCPUCount) { - this.defaultCPUCount = defaultCPUCount; - } - - public Integer getDefaultWalltime() { - return defaultWalltime; - } - - public void setDefaultWalltime(Integer defaultWalltime) { - this.defaultWalltime = defaultWalltime; - } - - @Override - public String toString() { - return "LocalComputeResourceEntity{" + - "computeResourceId='" + computeResourceId + '\'' + - ", hostName='" + hostName + '\'' + - ", enabled=" + enabled + - '}'; - } -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/LocalStorageResourceEntity.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/LocalStorageResourceEntity.java deleted file mode 100644 index b07206e88d2..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/LocalStorageResourceEntity.java +++ /dev/null @@ -1,124 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.entity; - -import jakarta.persistence.*; -import java.io.Serializable; -import java.sql.Timestamp; - -/** - * Local entity that mirrors airavata-api StorageResourceEntity structure - * Used for local development without external airavata-api dependencies - */ -@Entity -@Table(name = "LOCAL_STORAGE_RESOURCE") -public class LocalStorageResourceEntity implements Serializable { - private static final long serialVersionUID = 1L; - - @Id - @Column(name = "STORAGE_RESOURCE_ID") - private String storageResourceId; - - @Column(name = "CREATION_TIME") - private Timestamp creationTime; - - @Column(name = "DESCRIPTION", length = 4000) - private String storageResourceDescription; - - @Column(name = "ENABLED") - private boolean enabled; - - @Column(name = "HOST_NAME") - private String hostName; - - @Column(name = "UPDATE_TIME") - private Timestamp updateTime; - - // Default constructor - public LocalStorageResourceEntity() {} - - // Constructor matching airavata-api pattern - public LocalStorageResourceEntity(String storageResourceId, String hostName, String description, boolean enabled) { - this.storageResourceId = storageResourceId; - this.hostName = hostName; - this.storageResourceDescription = description; - this.enabled = enabled; - this.creationTime = new Timestamp(System.currentTimeMillis()); - this.updateTime = new Timestamp(System.currentTimeMillis()); - } - - // Getters and setters (matching airavata-api naming) - public String getStorageResourceId() { - return storageResourceId; - } - - public void setStorageResourceId(String storageResourceId) { - this.storageResourceId = storageResourceId; - } - - public String getStorageResourceDescription() { - return storageResourceDescription; - } - - public void setStorageResourceDescription(String storageResourceDescription) { - this.storageResourceDescription = storageResourceDescription; - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public String getHostName() { - return hostName; - } - - public void setHostName(String hostName) { - this.hostName = hostName; - } - - public Timestamp getCreationTime() { - return creationTime; - } - - public void setCreationTime(Timestamp creationTime) { - this.creationTime = creationTime; - } - - public Timestamp getUpdateTime() { - return updateTime; - } - - public void setUpdateTime(Timestamp updateTime) { - this.updateTime = updateTime; - } - - @Override - public String toString() { - return "LocalStorageResourceEntity{" + - "storageResourceId='" + storageResourceId + '\'' + - ", hostName='" + hostName + '\'' + - ", enabled=" + enabled + - '}'; - } -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/StorageResourceEntity.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/StorageResourceEntity.java new file mode 100644 index 00000000000..77b13083905 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/StorageResourceEntity.java @@ -0,0 +1,78 @@ +package org.apache.airavata.research.service.entity; + +import jakarta.persistence.*; +import java.io.Serializable; +import java.sql.Timestamp; + +@Entity +@Table(name = "STORAGE_RESOURCE") +public class StorageResourceEntity implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "STORAGE_RESOURCE_ID") + private String storageResourceId; + + @Column(name = "HOST_NAME", nullable = false) + private String hostName; + + @Column(name = "DESCRIPTION") + private String description; + + @Column(name = "ENABLED") + private Short enabled; + + @Column(name = "CREATION_TIME", nullable = false) + private Timestamp creationTime; + + @Column(name = "UPDATE_TIME", nullable = false) + private Timestamp updateTime; + + public String getStorageResourceId() { + return storageResourceId; + } + + public void setStorageResourceId(String storageResourceId) { + this.storageResourceId = storageResourceId; + } + + public String getHostName() { + return hostName; + } + + public void setHostName(String hostName) { + this.hostName = hostName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Short getEnabled() { + return enabled; + } + + public void setEnabled(Short enabled) { + this.enabled = enabled; + } + + public Timestamp getCreationTime() { + return creationTime; + } + + public void setCreationTime(Timestamp creationTime) { + this.creationTime = creationTime; + } + + public Timestamp getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Timestamp updateTime) { + this.updateTime = updateTime; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/ComputeResourceHandler.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/ComputeResourceHandler.java index 5f0dbd37da4..1b36101eeed 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/ComputeResourceHandler.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/ComputeResourceHandler.java @@ -18,252 +18,213 @@ */ package org.apache.airavata.research.service.handler; -import org.apache.airavata.model.appcatalog.computeresource.ComputeResourceDescription; -import org.apache.airavata.registry.api.RegistryService; -import org.apache.airavata.registry.api.exception.RegistryServiceException; -import org.apache.airavata.research.service.config.RegistryServiceConfig; +import org.apache.airavata.research.service.entity.ComputeResourceEntity; import org.apache.airavata.research.service.dto.ComputeResourceDTO; +import org.apache.airavata.research.service.repository.ComputeResourceRepository; import org.apache.airavata.research.service.util.DTOConverter; -import org.apache.thrift.TException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.sql.Timestamp; +import java.util.ArrayList; import java.util.List; -import java.util.Map; +import java.util.Optional; import java.util.UUID; -import java.util.stream.Collectors; /** - * Handler for Compute Resource operations using Airavata Registry Service - * Integrates with existing airavata-api infrastructure + * Handler for Compute Resource operations using local entities with app_catalog database + * Direct integration with Airavata app_catalog database */ -@Component +@Component("computeResourceHandler") public class ComputeResourceHandler { private static final Logger LOGGER = LoggerFactory.getLogger(ComputeResourceHandler.class); - @Autowired - private RegistryServiceConfig.RegistryServiceProvider registryServiceProvider; + private final ComputeResourceRepository computeResourceRepository; + private final DTOConverter dtoConverter; - @Autowired - private DTOConverter dtoConverter; - - /** - * Get RegistryService with connection validation - */ - private RegistryService.Iface getRegistryService() throws RegistryServiceException { - return registryServiceProvider.getRegistryService(); + public ComputeResourceHandler(ComputeResourceRepository computeResourceRepository, + DTOConverter dtoConverter) { + this.computeResourceRepository = computeResourceRepository; + this.dtoConverter = dtoConverter; } /** - * Check if registry service is available + * Get all enabled compute resources */ - private boolean isRegistryServiceAvailable() { - return registryServiceProvider.isAvailable(); - } - - /** - * Create a new compute resource using registry service - */ - public ComputeResourceDTO createComputeResource(ComputeResourceDTO dto) throws RegistryServiceException, TException { - LOGGER.debug("Creating compute resource: {}", dto.getHostName()); + public List getAllComputeResources() { + LOGGER.info("Getting all compute resources from app_catalog"); try { - // Convert DTO to Thrift model - ComputeResourceDescription thriftModel = dtoConverter.dtoToThrift(dto); + List entities = computeResourceRepository.findAllEnabledOrderByCreationTime(); + List dtos = new ArrayList<>(); - // Generate ID if not provided - if (thriftModel.getComputeResourceId() == null || thriftModel.getComputeResourceId().isEmpty()) { - thriftModel.setComputeResourceId(generateComputeResourceId()); + for (ComputeResourceEntity entity : entities) { + ComputeResourceDTO dto = dtoConverter.computeEntityToDTO(entity); + dtos.add(dto); } - // Use existing registry service to save - String resourceId = getRegistryService().registerComputeResource(thriftModel); - LOGGER.info("Successfully created compute resource with ID: {}", resourceId); - - // Retrieve saved entity with generated/updated fields - ComputeResourceDescription savedModel = getRegistryService().getComputeResource(resourceId); - - // Convert back to DTO for frontend - return dtoConverter.thriftToDTO(savedModel); - - } catch (RegistryServiceException e) { - LOGGER.error("Failed to create compute resource: {}", e.getMessage(), e); - throw e; - } catch (TException e) { - LOGGER.error("Thrift error creating compute resource: {}", e.getMessage(), e); - throw e; + LOGGER.info("Found {} compute resources from app_catalog", dtos.size()); + return dtos; } catch (Exception e) { - LOGGER.error("Unexpected error creating compute resource", e); - throw new RegistryServiceException("Failed to create compute resource: " + e.getMessage()); + LOGGER.error("Failed to get compute resources from app_catalog", e); + throw new RuntimeException("Failed to get compute resources", e); } } /** - * Get compute resource by ID + * Search compute resources by hostname */ - public ComputeResourceDTO getComputeResource(String resourceId) throws RegistryServiceException, TException { - LOGGER.debug("Retrieving compute resource: {}", resourceId); + public List searchComputeResources(String keyword) { + LOGGER.info("Searching compute resources in app_catalog with keyword: {}", keyword); try { - ComputeResourceDescription thriftModel = getRegistryService().getComputeResource(resourceId); - return dtoConverter.thriftToDTO(thriftModel); - } catch (RegistryServiceException e) { - LOGGER.error("Failed to get compute resource {}: {}", resourceId, e.getMessage()); - throw e; - } catch (TException e) { - LOGGER.error("Thrift error getting compute resource {}: {}", resourceId, e.getMessage()); - throw e; + List entities; + + if (keyword == null || keyword.trim().isEmpty()) { + entities = computeResourceRepository.findAllEnabledOrderByCreationTime(); + } else { + entities = computeResourceRepository.findEnabledByHostNameContaining(keyword.trim()); + } + + List dtos = new ArrayList<>(); + for (ComputeResourceEntity entity : entities) { + ComputeResourceDTO dto = dtoConverter.computeEntityToDTO(entity); + dtos.add(dto); + } + + LOGGER.info("Found {} compute resources matching keyword '{}'", dtos.size(), keyword); + return dtos; + } catch (Exception e) { + LOGGER.error("Failed to search compute resources in app_catalog", e); + throw new RuntimeException("Failed to search compute resources", e); } } /** - * Get all compute resources + * Get compute resource by ID */ - public List getAllComputeResources() throws RegistryServiceException, TException { - LOGGER.debug("Retrieving all compute resources"); + public ComputeResourceDTO getComputeResource(String computeResourceId) { + LOGGER.info("Getting compute resource by ID from app_catalog: {}", computeResourceId); try { - // Get compute resource names (ID -> Name mapping) - Map computeResourceNames = getRegistryService().getAllComputeResourceNames(); + Optional entityOpt = computeResourceRepository.findById(computeResourceId); + + if (entityOpt.isEmpty()) { + LOGGER.warn("Compute resource not found with ID: {}", computeResourceId); + throw new RuntimeException("Compute resource not found with ID: " + computeResourceId); + } + + ComputeResourceEntity entity = entityOpt.get(); + ComputeResourceDTO dto = dtoConverter.computeEntityToDTO(entity); - // Fetch full details for each compute resource - return computeResourceNames.keySet().stream() - .map(resourceId -> { - try { - return getRegistryService().getComputeResource(resourceId); - } catch (RegistryServiceException e) { - LOGGER.warn("Failed to get compute resource {}: {}", resourceId, e.getMessage()); - return null; - } catch (TException e) { - LOGGER.warn("Thrift error getting compute resource {}: {}", resourceId, e.getMessage()); - return null; - } - }) - .filter(thriftModel -> thriftModel != null) - .map(thriftModel -> dtoConverter.thriftToDTO(thriftModel)) - .collect(Collectors.toList()); - - } catch (RegistryServiceException e) { - LOGGER.error("Failed to get all compute resources: {}", e.getMessage()); - throw e; - } catch (TException e) { - LOGGER.error("Thrift error getting all compute resources: {}", e.getMessage()); - throw e; + LOGGER.info("Found compute resource: {}", entity.getHostName()); + return dto; + } catch (Exception e) { + LOGGER.error("Failed to get compute resource by ID: {}", computeResourceId, e); + throw new RuntimeException("Failed to get compute resource", e); } } /** - * Update compute resource + * Create new compute resource */ - public ComputeResourceDTO updateComputeResource(String resourceId, ComputeResourceDTO dto) throws RegistryServiceException, TException { - LOGGER.debug("Updating compute resource: {}", resourceId); + public ComputeResourceDTO createComputeResource(ComputeResourceDTO computeResourceDTO) { + LOGGER.info("Creating compute resource in app_catalog: {}", computeResourceDTO.getHostName()); try { - // Ensure DTO has the correct ID - dto.setComputeResourceId(resourceId); + // Convert DTO to entity using existing DTOConverter + ComputeResourceEntity entity = dtoConverter.computeResourceDTOToEntity(computeResourceDTO); - // Convert DTO to Thrift model - ComputeResourceDescription thriftModel = dtoConverter.dtoToThrift(dto); + // Set system fields + entity.setResourceId(UUID.randomUUID().toString()); + entity.setEnabled((short) 1); + entity.setCreationTime(new Timestamp(System.currentTimeMillis())); + entity.setUpdateTime(new Timestamp(System.currentTimeMillis())); - // Use existing registry service to update - boolean updated = getRegistryService().updateComputeResource(resourceId, thriftModel); + // Save to app_catalog database + ComputeResourceEntity savedEntity = computeResourceRepository.save(entity); - if (updated) { - LOGGER.info("Successfully updated compute resource: {}", resourceId); - - // Retrieve updated entity - ComputeResourceDescription updatedModel = getRegistryService().getComputeResource(resourceId); - return dtoConverter.thriftToDTO(updatedModel); - } else { - throw new RegistryServiceException("Failed to update compute resource: " + resourceId); - } + // Convert back to DTO + ComputeResourceDTO savedDTO = dtoConverter.computeEntityToDTO(savedEntity); - } catch (RegistryServiceException e) { - LOGGER.error("Failed to update compute resource {}: {}", resourceId, e.getMessage()); - throw e; - } catch (TException e) { - LOGGER.error("Thrift error updating compute resource {}: {}", resourceId, e.getMessage()); - throw e; + LOGGER.info("Created compute resource in app_catalog with ID: {}", savedEntity.getResourceId()); + return savedDTO; + } catch (Exception e) { + LOGGER.error("Failed to create compute resource in app_catalog", e); + throw new RuntimeException("Failed to create compute resource", e); } } /** - * Delete compute resource + * Update existing compute resource */ - public void deleteComputeResource(String resourceId) throws RegistryServiceException, TException { - LOGGER.debug("Deleting compute resource: {}", resourceId); + public ComputeResourceDTO updateComputeResource(String computeResourceId, ComputeResourceDTO computeResourceDTO) { + LOGGER.info("Updating compute resource in app_catalog: {}", computeResourceId); try { - boolean deleted = getRegistryService().deleteComputeResource(resourceId); + Optional existingOpt = computeResourceRepository.findById(computeResourceId); - if (deleted) { - LOGGER.info("Successfully deleted compute resource: {}", resourceId); - } else { - throw new RegistryServiceException("Failed to delete compute resource: " + resourceId); + if (existingOpt.isEmpty()) { + throw new RuntimeException("Compute resource not found with ID: " + computeResourceId); } - } catch (RegistryServiceException e) { - LOGGER.error("Failed to delete compute resource {}: {}", resourceId, e.getMessage()); - throw e; - } catch (TException e) { - LOGGER.error("Thrift error deleting compute resource {}: {}", resourceId, e.getMessage()); - throw e; + // Convert DTO to entity + ComputeResourceEntity updatedEntity = dtoConverter.computeResourceDTOToEntity(computeResourceDTO); + + // Preserve system fields + ComputeResourceEntity existing = existingOpt.get(); + updatedEntity.setResourceId(computeResourceId); + updatedEntity.setCreationTime(existing.getCreationTime()); + updatedEntity.setUpdateTime(new Timestamp(System.currentTimeMillis())); + + // Save updated entity + ComputeResourceEntity savedEntity = computeResourceRepository.save(updatedEntity); + + // Convert back to DTO + ComputeResourceDTO savedDTO = dtoConverter.computeEntityToDTO(savedEntity); + + LOGGER.info("Updated compute resource in app_catalog: {}", computeResourceId); + return savedDTO; + } catch (Exception e) { + LOGGER.error("Failed to update compute resource in app_catalog: {}", computeResourceId, e); + throw new RuntimeException("Failed to update compute resource", e); } } /** - * Search compute resources by keyword - * Note: This is a simplified implementation - Airavata registry might have more sophisticated search + * Delete compute resource */ - public List searchComputeResources(String keyword) throws RegistryServiceException, TException { - LOGGER.debug("Searching compute resources with keyword: {}", keyword); + public void deleteComputeResource(String computeResourceId) { + LOGGER.info("Deleting compute resource from app_catalog: {}", computeResourceId); try { - // Get all compute resources and filter by keyword - List allResources = getAllComputeResources(); + if (!computeResourceRepository.existsById(computeResourceId)) { + throw new RuntimeException("Compute resource not found with ID: " + computeResourceId); + } - String lowerKeyword = keyword.toLowerCase(); - return allResources.stream() - .filter(resource -> - (resource.getHostName() != null && resource.getHostName().toLowerCase().contains(lowerKeyword)) || - (resource.getResourceDescription() != null && resource.getResourceDescription().toLowerCase().contains(lowerKeyword)) || - (resource.getComputeType() != null && resource.getComputeType().toLowerCase().contains(lowerKeyword)) || - (resource.getOperatingSystem() != null && resource.getOperatingSystem().toLowerCase().contains(lowerKeyword)) - ) - .collect(Collectors.toList()); - - } catch (RegistryServiceException e) { - LOGGER.error("Failed to search compute resources: {}", e.getMessage()); - throw e; - } catch (TException e) { - LOGGER.error("Thrift error searching compute resources: {}", e.getMessage()); - throw e; + computeResourceRepository.deleteById(computeResourceId); + LOGGER.info("Deleted compute resource from app_catalog: {}", computeResourceId); + } catch (Exception e) { + LOGGER.error("Failed to delete compute resource from app_catalog: {}", computeResourceId, e); + throw new RuntimeException("Failed to delete compute resource", e); } } /** * Check if compute resource exists */ - public boolean existsComputeResource(String resourceId) { + public boolean existsComputeResource(String computeResourceId) { + LOGGER.debug("Checking if compute resource exists in app_catalog: {}", computeResourceId); + try { - ComputeResourceDescription resource = getRegistryService().getComputeResource(resourceId); - return resource != null; - } catch (RegistryServiceException e) { - LOGGER.debug("Compute resource {} does not exist: {}", resourceId, e.getMessage()); - return false; - } catch (TException e) { - LOGGER.debug("Thrift error checking compute resource {}: {}", resourceId, e.getMessage()); + boolean exists = computeResourceRepository.existsById(computeResourceId); + LOGGER.debug("Compute resource {} exists: {}", computeResourceId, exists); + return exists; + } catch (Exception e) { + LOGGER.error("Failed to check compute resource existence in app_catalog: {}", computeResourceId, e); return false; } } - - /** - * Generate unique compute resource ID - */ - private String generateComputeResourceId() { - return "compute_" + UUID.randomUUID().toString().replace("-", ""); - } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/LocalComputeResourceHandler.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/LocalComputeResourceHandler.java deleted file mode 100644 index b98c0bfb3a0..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/LocalComputeResourceHandler.java +++ /dev/null @@ -1,230 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.handler; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.sql.Timestamp; -import org.apache.airavata.research.service.entity.LocalComputeResourceEntity; -import org.apache.airavata.research.service.dto.ComputeResourceDTO; -import org.apache.airavata.research.service.repository.LocalComputeResourceRepository; -import org.apache.airavata.research.service.util.DTOConverter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -/** - * Local handler for Compute Resource operations using airavata-api entities with local database - * Alternative to external registry services for development - */ -@Component("localComputeResourceHandler") -public class LocalComputeResourceHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(LocalComputeResourceHandler.class); - - private final LocalComputeResourceRepository computeResourceRepository; - private final DTOConverter dtoConverter; - - public LocalComputeResourceHandler(LocalComputeResourceRepository computeResourceRepository, - DTOConverter dtoConverter) { - this.computeResourceRepository = computeResourceRepository; - this.dtoConverter = dtoConverter; - } - - /** - * Get all enabled compute resources - */ - public List getAllComputeResources() { - LOGGER.info("Getting all local compute resources"); - - try { - List entities = computeResourceRepository.findAllEnabledOrderByCreationTime(); - List dtos = new ArrayList<>(); - - for (LocalComputeResourceEntity entity : entities) { - ComputeResourceDTO dto = dtoConverter.computeEntityToDTO(entity); - dtos.add(dto); - } - - LOGGER.info("Found {} local compute resources", dtos.size()); - return dtos; - } catch (Exception e) { - LOGGER.error("Failed to get local compute resources", e); - throw new RuntimeException("Failed to get local compute resources", e); - } - } - - /** - * Search compute resources by keyword - */ - public List searchComputeResources(String keyword) { - LOGGER.info("Searching local compute resources with keyword: {}", keyword); - - try { - List entities; - - if (keyword == null || keyword.trim().isEmpty()) { - entities = computeResourceRepository.findAllEnabledOrderByCreationTime(); - } else { - entities = computeResourceRepository.searchEnabledComputeResources(keyword.trim()); - } - - List dtos = new ArrayList<>(); - for (LocalComputeResourceEntity entity : entities) { - ComputeResourceDTO dto = dtoConverter.computeEntityToDTO(entity); - dtos.add(dto); - } - - LOGGER.info("Found {} compute resources matching keyword '{}'", dtos.size(), keyword); - return dtos; - } catch (Exception e) { - LOGGER.error("Failed to search local compute resources", e); - throw new RuntimeException("Failed to search local compute resources", e); - } - } - - /** - * Get compute resource by ID - */ - public ComputeResourceDTO getComputeResource(String computeResourceId) { - LOGGER.info("Getting local compute resource by ID: {}", computeResourceId); - - try { - Optional entityOpt = computeResourceRepository.findById(computeResourceId); - - if (entityOpt.isEmpty()) { - LOGGER.warn("Compute resource not found with ID: {}", computeResourceId); - throw new RuntimeException("Compute resource not found with ID: " + computeResourceId); - } - - LocalComputeResourceEntity entity = entityOpt.get(); - ComputeResourceDTO dto = dtoConverter.computeEntityToDTO(entity); - - LOGGER.info("Found local compute resource: {}", entity.getHostName()); - return dto; - } catch (Exception e) { - LOGGER.error("Failed to get local compute resource by ID: {}", computeResourceId, e); - throw new RuntimeException("Failed to get local compute resource", e); - } - } - - /** - * Create new compute resource - */ - public ComputeResourceDTO createComputeResource(ComputeResourceDTO computeResourceDTO) { - LOGGER.info("Creating local compute resource: {}", computeResourceDTO.getHostName()); - - try { - // Convert DTO to entity using existing DTOConverter - LocalComputeResourceEntity entity = dtoConverter.computeResourceDTOToEntity(computeResourceDTO); - - // Set system fields - entity.setComputeResourceId(UUID.randomUUID().toString()); - entity.setEnabled((short) 1); - entity.setCreationTime(new Timestamp(System.currentTimeMillis())); - entity.setUpdateTime(new Timestamp(System.currentTimeMillis())); - - // Save to local database - LocalComputeResourceEntity savedEntity = computeResourceRepository.save(entity); - - // Convert back to DTO - ComputeResourceDTO savedDTO = dtoConverter.computeEntityToDTO(savedEntity); - - LOGGER.info("Created local compute resource with ID: {}", savedEntity.getComputeResourceId()); - return savedDTO; - } catch (Exception e) { - LOGGER.error("Failed to create local compute resource", e); - throw new RuntimeException("Failed to create local compute resource", e); - } - } - - /** - * Update existing compute resource - */ - public ComputeResourceDTO updateComputeResource(String computeResourceId, ComputeResourceDTO computeResourceDTO) { - LOGGER.info("Updating local compute resource: {}", computeResourceId); - - try { - Optional existingOpt = computeResourceRepository.findById(computeResourceId); - - if (existingOpt.isEmpty()) { - throw new RuntimeException("Compute resource not found with ID: " + computeResourceId); - } - - // Convert DTO to entity - LocalComputeResourceEntity updatedEntity = dtoConverter.computeResourceDTOToEntity(computeResourceDTO); - - // Preserve system fields - LocalComputeResourceEntity existing = existingOpt.get(); - updatedEntity.setComputeResourceId(computeResourceId); - updatedEntity.setCreationTime(existing.getCreationTime()); - updatedEntity.setUpdateTime(new Timestamp(System.currentTimeMillis())); - - // Save updated entity - LocalComputeResourceEntity savedEntity = computeResourceRepository.save(updatedEntity); - - // Convert back to DTO - ComputeResourceDTO savedDTO = dtoConverter.computeEntityToDTO(savedEntity); - - LOGGER.info("Updated local compute resource: {}", computeResourceId); - return savedDTO; - } catch (Exception e) { - LOGGER.error("Failed to update local compute resource: {}", computeResourceId, e); - throw new RuntimeException("Failed to update local compute resource", e); - } - } - - /** - * Delete compute resource - */ - public void deleteComputeResource(String computeResourceId) { - LOGGER.info("Deleting local compute resource: {}", computeResourceId); - - try { - if (!computeResourceRepository.existsById(computeResourceId)) { - throw new RuntimeException("Compute resource not found with ID: " + computeResourceId); - } - - computeResourceRepository.deleteById(computeResourceId); - LOGGER.info("Deleted local compute resource: {}", computeResourceId); - } catch (Exception e) { - LOGGER.error("Failed to delete local compute resource: {}", computeResourceId, e); - throw new RuntimeException("Failed to delete local compute resource", e); - } - } - - /** - * Check if compute resource exists - */ - public boolean existsComputeResource(String computeResourceId) { - LOGGER.debug("Checking if compute resource exists: {}", computeResourceId); - - try { - boolean exists = computeResourceRepository.existsById(computeResourceId); - LOGGER.debug("Compute resource {} exists: {}", computeResourceId, exists); - return exists; - } catch (Exception e) { - LOGGER.error("Failed to check compute resource existence: {}", computeResourceId, e); - return false; - } - } -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/LocalStorageResourceHandler.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/LocalStorageResourceHandler.java deleted file mode 100644 index 6b88ba48b4e..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/LocalStorageResourceHandler.java +++ /dev/null @@ -1,257 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.handler; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.sql.Timestamp; -import org.apache.airavata.research.service.entity.LocalStorageResourceEntity; -import org.apache.airavata.research.service.dto.StorageResourceDTO; -import org.apache.airavata.research.service.repository.LocalStorageResourceRepository; -import org.apache.airavata.research.service.util.DTOConverter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -/** - * Local handler for Storage Resource operations using airavata-api entities with local database - * Alternative to external registry services for development - */ -@Component("localStorageResourceHandler") -public class LocalStorageResourceHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(LocalStorageResourceHandler.class); - - private final LocalStorageResourceRepository storageResourceRepository; - private final DTOConverter dtoConverter; - - public LocalStorageResourceHandler(LocalStorageResourceRepository storageResourceRepository, - DTOConverter dtoConverter) { - this.storageResourceRepository = storageResourceRepository; - this.dtoConverter = dtoConverter; - } - - /** - * Get all enabled storage resources - */ - public List getAllStorageResources() { - LOGGER.info("Getting all local storage resources"); - - try { - List entities = storageResourceRepository.findAllEnabledOrderByCreationTime(); - List dtos = new ArrayList<>(); - - for (LocalStorageResourceEntity entity : entities) { - StorageResourceDTO dto = dtoConverter.storageEntityToDTO(entity); - dtos.add(dto); - } - - LOGGER.info("Found {} local storage resources", dtos.size()); - return dtos; - } catch (Exception e) { - LOGGER.error("Failed to get local storage resources", e); - throw new RuntimeException("Failed to get local storage resources", e); - } - } - - /** - * Search storage resources by keyword - */ - public List searchStorageResources(String keyword) { - LOGGER.info("Searching local storage resources with keyword: {}", keyword); - - try { - List entities; - - if (keyword == null || keyword.trim().isEmpty()) { - entities = storageResourceRepository.findAllEnabledOrderByCreationTime(); - } else { - entities = storageResourceRepository.searchEnabledStorageResources(keyword.trim()); - } - - List dtos = new ArrayList<>(); - for (LocalStorageResourceEntity entity : entities) { - StorageResourceDTO dto = dtoConverter.storageEntityToDTO(entity); - dtos.add(dto); - } - - LOGGER.info("Found {} storage resources matching keyword '{}'", dtos.size(), keyword); - return dtos; - } catch (Exception e) { - LOGGER.error("Failed to search local storage resources", e); - throw new RuntimeException("Failed to search local storage resources", e); - } - } - - /** - * Get storage resource by ID - */ - public StorageResourceDTO getStorageResource(String storageResourceId) { - LOGGER.info("Getting local storage resource by ID: {}", storageResourceId); - - try { - Optional entityOpt = storageResourceRepository.findById(storageResourceId); - - if (entityOpt.isEmpty()) { - LOGGER.warn("Storage resource not found with ID: {}", storageResourceId); - throw new RuntimeException("Storage resource not found with ID: " + storageResourceId); - } - - LocalStorageResourceEntity entity = entityOpt.get(); - StorageResourceDTO dto = dtoConverter.storageEntityToDTO(entity); - - LOGGER.info("Found local storage resource: {}", entity.getHostName()); - return dto; - } catch (Exception e) { - LOGGER.error("Failed to get local storage resource by ID: {}", storageResourceId, e); - throw new RuntimeException("Failed to get local storage resource", e); - } - } - - /** - * Create new storage resource - */ - public StorageResourceDTO createStorageResource(StorageResourceDTO storageResourceDTO) { - LOGGER.info("Creating local storage resource: {}", storageResourceDTO.getHostName()); - - try { - // Convert DTO to entity using existing DTOConverter - LocalStorageResourceEntity entity = dtoConverter.storageResourceDTOToEntity(storageResourceDTO); - - // Set system fields - entity.setStorageResourceId(UUID.randomUUID().toString()); - entity.setEnabled(true); - entity.setCreationTime(new Timestamp(System.currentTimeMillis())); - entity.setUpdateTime(new Timestamp(System.currentTimeMillis())); - - // Save to local database - LocalStorageResourceEntity savedEntity = storageResourceRepository.save(entity); - - // Convert back to DTO - StorageResourceDTO savedDTO = dtoConverter.storageEntityToDTO(savedEntity); - - LOGGER.info("Created local storage resource with ID: {}", savedEntity.getStorageResourceId()); - return savedDTO; - } catch (Exception e) { - LOGGER.error("Failed to create local storage resource", e); - throw new RuntimeException("Failed to create local storage resource", e); - } - } - - /** - * Update existing storage resource - */ - public StorageResourceDTO updateStorageResource(String storageResourceId, StorageResourceDTO storageResourceDTO) { - LOGGER.info("Updating local storage resource: {}", storageResourceId); - - try { - Optional existingOpt = storageResourceRepository.findById(storageResourceId); - - if (existingOpt.isEmpty()) { - throw new RuntimeException("Storage resource not found with ID: " + storageResourceId); - } - - // Convert DTO to entity - LocalStorageResourceEntity updatedEntity = dtoConverter.storageResourceDTOToEntity(storageResourceDTO); - - // Preserve system fields - LocalStorageResourceEntity existing = existingOpt.get(); - updatedEntity.setStorageResourceId(storageResourceId); - updatedEntity.setCreationTime(existing.getCreationTime()); - updatedEntity.setUpdateTime(new Timestamp(System.currentTimeMillis())); - - // Save updated entity - LocalStorageResourceEntity savedEntity = storageResourceRepository.save(updatedEntity); - - // Convert back to DTO - StorageResourceDTO savedDTO = dtoConverter.storageEntityToDTO(savedEntity); - - LOGGER.info("Updated local storage resource: {}", storageResourceId); - return savedDTO; - } catch (Exception e) { - LOGGER.error("Failed to update local storage resource: {}", storageResourceId, e); - throw new RuntimeException("Failed to update local storage resource", e); - } - } - - /** - * Delete storage resource - */ - public void deleteStorageResource(String storageResourceId) { - LOGGER.info("Deleting local storage resource: {}", storageResourceId); - - try { - if (!storageResourceRepository.existsById(storageResourceId)) { - throw new RuntimeException("Storage resource not found with ID: " + storageResourceId); - } - - storageResourceRepository.deleteById(storageResourceId); - LOGGER.info("Deleted local storage resource: {}", storageResourceId); - } catch (Exception e) { - LOGGER.error("Failed to delete local storage resource: {}", storageResourceId, e); - throw new RuntimeException("Failed to delete local storage resource", e); - } - } - - /** - * Get storage resources by storage type - */ - public List getStorageResourcesByType(String storageType) { - LOGGER.info("Getting local storage resources by type: {}", storageType); - - try { - List entities = storageResourceRepository.findAllEnabledOrderByCreationTime(); - List dtos = new ArrayList<>(); - - for (LocalStorageResourceEntity entity : entities) { - StorageResourceDTO dto = dtoConverter.storageEntityToDTO(entity); - // Filter by storage type from UI fields - if (storageType == null || storageType.isEmpty() || - (dto.getStorageType() != null && dto.getStorageType().equalsIgnoreCase(storageType))) { - dtos.add(dto); - } - } - - LOGGER.info("Found {} storage resources of type '{}'", dtos.size(), storageType); - return dtos; - } catch (Exception e) { - LOGGER.error("Failed to get storage resources by type", e); - throw new RuntimeException("Failed to get storage resources by type", e); - } - } - - /** - * Check if storage resource exists - */ - public boolean existsStorageResource(String storageResourceId) { - LOGGER.debug("Checking if storage resource exists: {}", storageResourceId); - - try { - boolean exists = storageResourceRepository.existsById(storageResourceId); - LOGGER.debug("Storage resource {} exists: {}", storageResourceId, exists); - return exists; - } catch (Exception e) { - LOGGER.error("Failed to check storage resource existence: {}", storageResourceId, e); - return false; - } - } -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/StorageResourceHandler.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/StorageResourceHandler.java index 3e03620fc26..c7731be812b 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/StorageResourceHandler.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/StorageResourceHandler.java @@ -18,283 +18,241 @@ */ package org.apache.airavata.research.service.handler; -import org.apache.airavata.model.appcatalog.storageresource.StorageResourceDescription; -import org.apache.airavata.registry.api.RegistryService; -import org.apache.airavata.registry.api.exception.RegistryServiceException; -import org.apache.airavata.research.service.config.RegistryServiceConfig; +import org.apache.airavata.research.service.entity.StorageResourceEntity; import org.apache.airavata.research.service.dto.StorageResourceDTO; +import org.apache.airavata.research.service.repository.StorageResourceRepository; import org.apache.airavata.research.service.util.DTOConverter; -import org.apache.thrift.TException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.sql.Timestamp; +import java.util.ArrayList; import java.util.List; -import java.util.Map; +import java.util.Optional; import java.util.UUID; -import java.util.stream.Collectors; /** - * Handler for Storage Resource operations using Airavata Registry Service - * Integrates with existing airavata-api infrastructure + * Handler for Storage Resource operations using local entities with app_catalog database + * Direct integration with Airavata app_catalog database */ -@Component +@Component("storageResourceHandler") public class StorageResourceHandler { private static final Logger LOGGER = LoggerFactory.getLogger(StorageResourceHandler.class); - @Autowired - private RegistryServiceConfig.RegistryServiceProvider registryServiceProvider; + private final StorageResourceRepository storageResourceRepository; + private final DTOConverter dtoConverter; - @Autowired - private DTOConverter dtoConverter; - - /** - * Get RegistryService with connection validation - */ - private RegistryService.Iface getRegistryService() throws RegistryServiceException { - return registryServiceProvider.getRegistryService(); - } - - /** - * Check if registry service is available - */ - private boolean isRegistryServiceAvailable() { - return registryServiceProvider.isAvailable(); + public StorageResourceHandler(StorageResourceRepository storageResourceRepository, + DTOConverter dtoConverter) { + this.storageResourceRepository = storageResourceRepository; + this.dtoConverter = dtoConverter; } /** - * Create a new storage resource using registry service + * Get all enabled storage resources */ - public StorageResourceDTO createStorageResource(StorageResourceDTO dto) throws RegistryServiceException, TException { - LOGGER.debug("Creating storage resource: {}", dto.getHostName()); + public List getAllStorageResources() { + LOGGER.info("Getting all storage resources from app_catalog"); try { - // Convert DTO to Thrift model - StorageResourceDescription thriftModel = dtoConverter.dtoToThrift(dto); + List entities = storageResourceRepository.findAllEnabledOrderByCreationTime(); + List dtos = new ArrayList<>(); - // Generate ID if not provided - if (thriftModel.getStorageResourceId() == null || thriftModel.getStorageResourceId().isEmpty()) { - thriftModel.setStorageResourceId(generateStorageResourceId()); + for (StorageResourceEntity entity : entities) { + StorageResourceDTO dto = dtoConverter.storageEntityToDTO(entity); + dtos.add(dto); } - // Set timestamps - long currentTime = System.currentTimeMillis(); - thriftModel.setCreationTime(currentTime); - thriftModel.setUpdateTime(currentTime); - - // Use existing registry service to save - String resourceId = getRegistryService().registerStorageResource(thriftModel); - LOGGER.info("Successfully created storage resource with ID: {}", resourceId); - - // Retrieve saved entity with generated/updated fields - StorageResourceDescription savedModel = getRegistryService().getStorageResource(resourceId); - - // Convert back to DTO for frontend - return dtoConverter.thriftToDTO(savedModel); - - } catch (RegistryServiceException e) { - LOGGER.error("Failed to create storage resource: {}", e.getMessage(), e); - throw e; - } catch (TException e) { - LOGGER.error("Thrift error creating storage resource: {}", e.getMessage(), e); - throw e; + LOGGER.info("Found {} storage resources from app_catalog", dtos.size()); + return dtos; } catch (Exception e) { - LOGGER.error("Unexpected error creating storage resource", e); - throw new RegistryServiceException("Failed to create storage resource: " + e.getMessage()); + LOGGER.error("Failed to get storage resources from app_catalog", e); + throw new RuntimeException("Failed to get storage resources", e); } } /** - * Get storage resource by ID + * Search storage resources by keyword */ - public StorageResourceDTO getStorageResource(String resourceId) throws RegistryServiceException, TException { - LOGGER.debug("Retrieving storage resource: {}", resourceId); + public List searchStorageResources(String keyword) { + LOGGER.info("Searching storage resources in app_catalog with keyword: {}", keyword); try { - StorageResourceDescription thriftModel = getRegistryService().getStorageResource(resourceId); - return dtoConverter.thriftToDTO(thriftModel); - } catch (RegistryServiceException e) { - LOGGER.error("Failed to get storage resource {}: {}", resourceId, e.getMessage()); - throw e; - } catch (TException e) { - LOGGER.error("Thrift error getting storage resource {}: {}", resourceId, e.getMessage()); - throw e; + List entities; + + if (keyword == null || keyword.trim().isEmpty()) { + entities = storageResourceRepository.findAllEnabledOrderByCreationTime(); + } else { + entities = storageResourceRepository.findEnabledByHostNameContaining(keyword.trim()); + } + + List dtos = new ArrayList<>(); + for (StorageResourceEntity entity : entities) { + StorageResourceDTO dto = dtoConverter.storageEntityToDTO(entity); + dtos.add(dto); + } + + LOGGER.info("Found {} storage resources matching keyword '{}'", dtos.size(), keyword); + return dtos; + } catch (Exception e) { + LOGGER.error("Failed to search storage resources in app_catalog", e); + throw new RuntimeException("Failed to search storage resources", e); } } + /** - * Get all storage resources + * Get storage resource by ID */ - public List getAllStorageResources() throws RegistryServiceException, TException { - LOGGER.debug("Retrieving all storage resources"); + public StorageResourceDTO getStorageResource(String storageResourceId) { + LOGGER.info("Getting storage resource by ID from app_catalog: {}", storageResourceId); try { - // Get storage resource names (ID -> Name mapping) - Map storageResourceNames = getRegistryService().getAllStorageResourceNames(); + Optional entityOpt = storageResourceRepository.findById(storageResourceId); + + if (entityOpt.isEmpty()) { + LOGGER.warn("Storage resource not found with ID: {}", storageResourceId); + throw new RuntimeException("Storage resource not found with ID: " + storageResourceId); + } + + StorageResourceEntity entity = entityOpt.get(); + StorageResourceDTO dto = dtoConverter.storageEntityToDTO(entity); - // Fetch full details for each storage resource - return storageResourceNames.keySet().stream() - .map(resourceId -> { - try { - return getRegistryService().getStorageResource(resourceId); - } catch (RegistryServiceException e) { - LOGGER.warn("Failed to get storage resource {}: {}", resourceId, e.getMessage()); - return null; - } catch (TException e) { - LOGGER.warn("Thrift error getting storage resource {}: {}", resourceId, e.getMessage()); - return null; - } - }) - .filter(thriftModel -> thriftModel != null) - .map(thriftModel -> dtoConverter.thriftToDTO(thriftModel)) - .collect(Collectors.toList()); - - } catch (RegistryServiceException e) { - LOGGER.error("Failed to get all storage resources: {}", e.getMessage()); - throw e; - } catch (TException e) { - LOGGER.error("Thrift error getting all storage resources: {}", e.getMessage()); - throw e; + LOGGER.info("Found storage resource: {}", entity.getHostName()); + return dto; + } catch (Exception e) { + LOGGER.error("Failed to get storage resource by ID: {}", storageResourceId, e); + throw new RuntimeException("Failed to get storage resource", e); } } /** - * Update storage resource + * Create new storage resource */ - public StorageResourceDTO updateStorageResource(String resourceId, StorageResourceDTO dto) throws RegistryServiceException, TException { - LOGGER.debug("Updating storage resource: {}", resourceId); + public StorageResourceDTO createStorageResource(StorageResourceDTO storageResourceDTO) { + LOGGER.info("Creating storage resource in app_catalog: {}", storageResourceDTO.getHostName()); try { - // Ensure DTO has the correct ID - dto.setStorageResourceId(resourceId); + // Convert DTO to entity using existing DTOConverter + StorageResourceEntity entity = dtoConverter.storageResourceDTOToEntity(storageResourceDTO); - // Convert DTO to Thrift model - StorageResourceDescription thriftModel = dtoConverter.dtoToThrift(dto); + // Set system fields + entity.setStorageResourceId(UUID.randomUUID().toString()); + entity.setEnabled((short) 1); + entity.setCreationTime(new Timestamp(System.currentTimeMillis())); + entity.setUpdateTime(new Timestamp(System.currentTimeMillis())); - // Set update timestamp - thriftModel.setUpdateTime(System.currentTimeMillis()); + // Save to app_catalog database + StorageResourceEntity savedEntity = storageResourceRepository.save(entity); - // Use existing registry service to update - boolean updated = getRegistryService().updateStorageResource(resourceId, thriftModel); + // Convert back to DTO + StorageResourceDTO savedDTO = dtoConverter.storageEntityToDTO(savedEntity); - if (updated) { - LOGGER.info("Successfully updated storage resource: {}", resourceId); - - // Retrieve updated entity - StorageResourceDescription updatedModel = getRegistryService().getStorageResource(resourceId); - return dtoConverter.thriftToDTO(updatedModel); - } else { - throw new RegistryServiceException("Failed to update storage resource: " + resourceId); - } - - } catch (RegistryServiceException e) { - LOGGER.error("Failed to update storage resource {}: {}", resourceId, e.getMessage()); - throw e; - } catch (TException e) { - LOGGER.error("Thrift error updating storage resource {}: {}", resourceId, e.getMessage()); - throw e; + LOGGER.info("Created storage resource in app_catalog with ID: {}", savedEntity.getStorageResourceId()); + return savedDTO; + } catch (Exception e) { + LOGGER.error("Failed to create storage resource in app_catalog", e); + throw new RuntimeException("Failed to create storage resource", e); } } /** - * Delete storage resource + * Update existing storage resource */ - public void deleteStorageResource(String resourceId) throws RegistryServiceException, TException { - LOGGER.debug("Deleting storage resource: {}", resourceId); + public StorageResourceDTO updateStorageResource(String storageResourceId, StorageResourceDTO storageResourceDTO) { + LOGGER.info("Updating storage resource in app_catalog: {}", storageResourceId); try { - boolean deleted = getRegistryService().deleteStorageResource(resourceId); + Optional existingOpt = storageResourceRepository.findById(storageResourceId); - if (deleted) { - LOGGER.info("Successfully deleted storage resource: {}", resourceId); - } else { - throw new RegistryServiceException("Failed to delete storage resource: " + resourceId); + if (existingOpt.isEmpty()) { + throw new RuntimeException("Storage resource not found with ID: " + storageResourceId); } - } catch (RegistryServiceException e) { - LOGGER.error("Failed to delete storage resource {}: {}", resourceId, e.getMessage()); - throw e; - } catch (TException e) { - LOGGER.error("Thrift error deleting storage resource {}: {}", resourceId, e.getMessage()); - throw e; + // Convert DTO to entity + StorageResourceEntity updatedEntity = dtoConverter.storageResourceDTOToEntity(storageResourceDTO); + + // Preserve system fields + StorageResourceEntity existing = existingOpt.get(); + updatedEntity.setStorageResourceId(storageResourceId); + updatedEntity.setCreationTime(existing.getCreationTime()); + updatedEntity.setUpdateTime(new Timestamp(System.currentTimeMillis())); + + // Save updated entity + StorageResourceEntity savedEntity = storageResourceRepository.save(updatedEntity); + + // Convert back to DTO + StorageResourceDTO savedDTO = dtoConverter.storageEntityToDTO(savedEntity); + + LOGGER.info("Updated storage resource in app_catalog: {}", storageResourceId); + return savedDTO; + } catch (Exception e) { + LOGGER.error("Failed to update storage resource in app_catalog: {}", storageResourceId, e); + throw new RuntimeException("Failed to update storage resource", e); } } /** - * Search storage resources by keyword - * Note: This is a simplified implementation - Airavata registry might have more sophisticated search + * Delete storage resource */ - public List searchStorageResources(String keyword) throws RegistryServiceException, TException { - LOGGER.debug("Searching storage resources with keyword: {}", keyword); + public void deleteStorageResource(String storageResourceId) { + LOGGER.info("Deleting storage resource from app_catalog: {}", storageResourceId); try { - // Get all storage resources and filter by keyword - List allResources = getAllStorageResources(); + if (!storageResourceRepository.existsById(storageResourceId)) { + throw new RuntimeException("Storage resource not found with ID: " + storageResourceId); + } - String lowerKeyword = keyword.toLowerCase(); - return allResources.stream() - .filter(resource -> - (resource.getHostName() != null && resource.getHostName().toLowerCase().contains(lowerKeyword)) || - (resource.getStorageResourceDescription() != null && resource.getStorageResourceDescription().toLowerCase().contains(lowerKeyword)) || - (resource.getStorageType() != null && resource.getStorageType().toLowerCase().contains(lowerKeyword)) || - (resource.getAccessProtocol() != null && resource.getAccessProtocol().toLowerCase().contains(lowerKeyword)) - ) - .collect(Collectors.toList()); - - } catch (RegistryServiceException e) { - LOGGER.error("Failed to search storage resources: {}", e.getMessage()); - throw e; - } catch (TException e) { - LOGGER.error("Thrift error searching storage resources: {}", e.getMessage()); - throw e; + storageResourceRepository.deleteById(storageResourceId); + LOGGER.info("Deleted storage resource from app_catalog: {}", storageResourceId); + } catch (Exception e) { + LOGGER.error("Failed to delete storage resource from app_catalog: {}", storageResourceId, e); + throw new RuntimeException("Failed to delete storage resource", e); } } /** - * Filter storage resources by type + * Get storage resources by type */ - public List getStorageResourcesByType(String storageType) throws RegistryServiceException, TException { - LOGGER.debug("Filtering storage resources by type: {}", storageType); + public List getStorageResourcesByType(String storageType) { + LOGGER.info("Getting storage resources by type from app_catalog: {}", storageType); try { - List allResources = getAllStorageResources(); + List entities = storageResourceRepository.findAllEnabledOrderByCreationTime(); + List dtos = new ArrayList<>(); + + for (StorageResourceEntity entity : entities) { + StorageResourceDTO dto = dtoConverter.storageEntityToDTO(entity); + // Filter by storage type from UI fields + if (storageType == null || storageType.isEmpty() || + (dto.getStorageType() != null && dto.getStorageType().equalsIgnoreCase(storageType))) { + dtos.add(dto); + } + } - return allResources.stream() - .filter(resource -> resource.getStorageType() != null && - resource.getStorageType().equalsIgnoreCase(storageType)) - .collect(Collectors.toList()); - - } catch (RegistryServiceException e) { - LOGGER.error("Failed to filter storage resources by type: {}", e.getMessage()); - throw e; - } catch (TException e) { - LOGGER.error("Thrift error filtering storage resources by type: {}", e.getMessage()); - throw e; + LOGGER.info("Found {} storage resources of type '{}'", dtos.size(), storageType); + return dtos; + } catch (Exception e) { + LOGGER.error("Failed to get storage resources by type from app_catalog", e); + throw new RuntimeException("Failed to get storage resources by type", e); } } /** * Check if storage resource exists */ - public boolean existsStorageResource(String resourceId) { + public boolean existsStorageResource(String storageResourceId) { + LOGGER.debug("Checking if storage resource exists in app_catalog: {}", storageResourceId); + try { - StorageResourceDescription resource = getRegistryService().getStorageResource(resourceId); - return resource != null; - } catch (RegistryServiceException e) { - LOGGER.debug("Storage resource {} does not exist: {}", resourceId, e.getMessage()); - return false; - } catch (TException e) { - LOGGER.debug("Thrift error checking storage resource {}: {}", resourceId, e.getMessage()); + boolean exists = storageResourceRepository.existsById(storageResourceId); + LOGGER.debug("Storage resource {} exists: {}", storageResourceId, exists); + return exists; + } catch (Exception e) { + LOGGER.error("Failed to check storage resource existence in app_catalog: {}", storageResourceId, e); return false; } } - - /** - * Generate unique storage resource ID - */ - private String generateStorageResourceId() { - return "storage_" + UUID.randomUUID().toString().replace("-", ""); - } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Tag.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Tag.java index 457f9ceb8f6..a1c617eface 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Tag.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Tag.java @@ -19,6 +19,7 @@ */ package org.apache.airavata.research.service.model.entity; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -37,6 +38,7 @@ public class Tag { private String id; @Column(name = "tag_value", nullable = false) + @JsonProperty("name") private String value; public String getId() { @@ -47,6 +49,7 @@ public void setId(String id) { this.id = id; } + @JsonProperty("name") public String getValue() { return value; } diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/ComputeResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/ComputeResourceRepository.java new file mode 100644 index 00000000000..499bb734aef --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/ComputeResourceRepository.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.airavata.research.service.repository; + +import org.apache.airavata.research.service.entity.ComputeResourceEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ComputeResourceRepository extends JpaRepository { + + @Query("SELECT c FROM ComputeResourceEntity c WHERE c.enabled = 1 ORDER BY c.creationTime DESC") + List findAllEnabledOrderByCreationTime(); + + @Query("SELECT c FROM ComputeResourceEntity c WHERE c.enabled = 1 AND c.hostName LIKE %:hostname%") + List findEnabledByHostNameContaining(@Param("hostname") String hostname); +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/LocalComputeResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/LocalComputeResourceRepository.java deleted file mode 100644 index 3b1b31edf67..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/LocalComputeResourceRepository.java +++ /dev/null @@ -1,55 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.repository; - -import java.util.List; -import org.apache.airavata.research.service.entity.LocalComputeResourceEntity; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -/** - * Local JPA Repository for airavata-api LocalComputeResourceEntity - * Used for local development data instead of external registry services - */ -@Repository -public interface LocalComputeResourceRepository extends JpaRepository { - - // Find enabled compute resources - List findByEnabled(short enabled); - - // Search by hostname - List findByHostNameContainingIgnoreCase(String hostName); - - // Search by description - List findByResourceDescriptionContainingIgnoreCase(String description); - - // Combined search functionality - @Query("SELECT c FROM LocalComputeResourceEntity c WHERE " + - "c.enabled = 1 AND (" + - "LOWER(c.hostName) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + - "LOWER(c.resourceDescription) LIKE LOWER(CONCAT('%', :keyword, '%')))") - List searchEnabledComputeResources(@Param("keyword") String keyword); - - // Get all enabled compute resources (for public API) - @Query("SELECT c FROM LocalComputeResourceEntity c WHERE c.enabled = 1 ORDER BY c.creationTime DESC") - List findAllEnabledOrderByCreationTime(); -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/LocalStorageResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/LocalStorageResourceRepository.java deleted file mode 100644 index 745c28fae4c..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/LocalStorageResourceRepository.java +++ /dev/null @@ -1,55 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.repository; - -import java.util.List; -import org.apache.airavata.research.service.entity.LocalStorageResourceEntity; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -/** - * Local JPA Repository for airavata-api StorageResourceEntity - * Used for local development data instead of external registry services - */ -@Repository -public interface LocalStorageResourceRepository extends JpaRepository { - - // Find enabled storage resources - List findByEnabled(boolean enabled); - - // Search by hostname - List findByHostNameContainingIgnoreCase(String hostName); - - // Search by description - List findByStorageResourceDescriptionContainingIgnoreCase(String description); - - // Combined search functionality - @Query("SELECT s FROM LocalStorageResourceEntity s WHERE " + - "s.enabled = true AND (" + - "LOWER(s.hostName) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + - "LOWER(s.storageResourceDescription) LIKE LOWER(CONCAT('%', :keyword, '%')))") - List searchEnabledStorageResources(@Param("keyword") String keyword); - - // Get all enabled storage resources (for public API) - @Query("SELECT s FROM LocalStorageResourceEntity s WHERE s.enabled = true ORDER BY s.creationTime DESC") - List findAllEnabledOrderByCreationTime(); -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/StorageResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/StorageResourceRepository.java new file mode 100644 index 00000000000..58f908c139b --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/StorageResourceRepository.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.airavata.research.service.repository; + +import org.apache.airavata.research.service.entity.StorageResourceEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface StorageResourceRepository extends JpaRepository { + + @Query("SELECT s FROM StorageResourceEntity s WHERE s.enabled = 1 ORDER BY s.creationTime DESC") + List findAllEnabledOrderByCreationTime(); + + @Query("SELECT s FROM StorageResourceEntity s WHERE s.enabled = 1 AND s.hostName LIKE %:hostname%") + List findEnabledByHostNameContaining(@Param("hostname") String hostname); +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java index ee190026ab3..79a6068853f 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java @@ -21,11 +21,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.airavata.model.appcatalog.computeresource.ComputeResourceDescription; import org.apache.airavata.model.appcatalog.computeresource.BatchQueue; +import org.apache.airavata.model.appcatalog.computeresource.ComputeResourceDescription; import org.apache.airavata.model.appcatalog.storageresource.StorageResourceDescription; -import org.apache.airavata.research.service.entity.LocalComputeResourceEntity; -import org.apache.airavata.research.service.entity.LocalStorageResourceEntity; +import org.apache.airavata.research.service.entity.ComputeResourceEntity; +import org.apache.airavata.research.service.entity.StorageResourceEntity; import org.apache.airavata.research.service.dto.ComputeResourceDTO; import org.apache.airavata.research.service.dto.ComputeResourceQueueDTO; import org.apache.airavata.research.service.dto.StorageResourceDTO; @@ -37,7 +37,7 @@ import java.util.stream.Collectors; /** - * Utility class for converting between Airavata Thrift models and UI DTOs + * Utility class for converting between entities and DTOs * Handles JSON serialization of UI-specific fields into description fields */ @Component @@ -471,9 +471,9 @@ private Boolean getBooleanValue(JsonNode node, String key) { // =============================== /** - * Convert LocalStorageResourceEntity (JPA) to StorageResourceDTO + * Convert StorageResourceEntity (JPA) to StorageResourceDTO */ - public StorageResourceDTO storageEntityToDTO(LocalStorageResourceEntity entity) { + public StorageResourceDTO storageEntityToDTO(StorageResourceEntity entity) { if (entity == null) { return null; } @@ -483,11 +483,13 @@ public StorageResourceDTO storageEntityToDTO(LocalStorageResourceEntity entity) // Core fields dto.setStorageResourceId(entity.getStorageResourceId()); dto.setHostName(entity.getHostName()); - dto.setStorageResourceDescription(entity.getStorageResourceDescription()); - dto.setEnabled(entity.isEnabled()); + dto.setStorageResourceDescription(entity.getDescription()); + // Handle enabled field safely - Short type from database + Short enabledValue = entity.getEnabled(); + dto.setEnabled(enabledValue != null && enabledValue.shortValue() == 1); // Generate a name from hostname if not available - dto.setName(generateStorageResourceName(entity.getHostName(), entity.getStorageResourceDescription())); + dto.setName(generateStorageResourceName(entity.getHostName(), entity.getDescription())); // Timestamps if (entity.getCreationTime() != null) { @@ -498,37 +500,37 @@ public StorageResourceDTO storageEntityToDTO(LocalStorageResourceEntity entity) } // Extract UI-specific fields from description JSON - extractStorageUIFieldsFromDescription(entity.getStorageResourceDescription(), dto); + extractStorageUIFieldsFromDescription(entity.getDescription(), dto); return dto; } /** - * Convert StorageResourceDTO to LocalStorageResourceEntity (JPA) + * Convert StorageResourceDTO to StorageResourceEntity (JPA) */ - public LocalStorageResourceEntity storageResourceDTOToEntity(StorageResourceDTO dto) { + public StorageResourceEntity storageResourceDTOToEntity(StorageResourceDTO dto) { if (dto == null) { return null; } - LocalStorageResourceEntity entity = new LocalStorageResourceEntity(); + StorageResourceEntity entity = new StorageResourceEntity(); // Core fields entity.setStorageResourceId(dto.getStorageResourceId()); entity.setHostName(dto.getHostName()); - entity.setEnabled(dto.isEnabled()); + entity.setEnabled(dto.isEnabled() ? (short) 1 : (short) 0); // Embed UI fields into description as JSON String descriptionWithUIFields = encodeStorageUIFieldsIntoDescription(dto); - entity.setStorageResourceDescription(descriptionWithUIFields); + entity.setDescription(descriptionWithUIFields); return entity; } /** - * Convert LocalComputeResourceEntity (JPA) to ComputeResourceDTO + * Convert ComputeResourceEntity (JPA) to ComputeResourceDTO */ - public ComputeResourceDTO computeEntityToDTO(LocalComputeResourceEntity entity) { + public ComputeResourceDTO computeEntityToDTO(ComputeResourceEntity entity) { if (entity == null) { return null; } @@ -536,12 +538,14 @@ public ComputeResourceDTO computeEntityToDTO(LocalComputeResourceEntity entity) ComputeResourceDTO dto = new ComputeResourceDTO(); // Core fields - dto.setComputeResourceId(entity.getComputeResourceId()); + dto.setComputeResourceId(entity.getResourceId()); dto.setHostName(entity.getHostName()); dto.setResourceDescription(entity.getResourceDescription()); - dto.setEnabled(entity.getEnabled() == 1); + // Handle enabled field safely - Short type from database + Short enabledValue = entity.getEnabled(); + dto.setEnabled(enabledValue != null && enabledValue.shortValue() == 1); dto.setCpuCores(entity.getCpusPerNode()); - dto.setMemoryGB(entity.getMaxMemoryPerNode()); + dto.setMemoryGB(entity.getMaxMemoryNode()); // Generate a name from hostname if not available dto.setName(generateComputeResourceName(entity.getHostName(), entity.getResourceDescription())); @@ -561,21 +565,21 @@ public ComputeResourceDTO computeEntityToDTO(LocalComputeResourceEntity entity) } /** - * Convert ComputeResourceDTO to LocalComputeResourceEntity (JPA) + * Convert ComputeResourceDTO to ComputeResourceEntity (JPA) */ - public LocalComputeResourceEntity computeResourceDTOToEntity(ComputeResourceDTO dto) { + public ComputeResourceEntity computeResourceDTOToEntity(ComputeResourceDTO dto) { if (dto == null) { return null; } - LocalComputeResourceEntity entity = new LocalComputeResourceEntity(); + ComputeResourceEntity entity = new ComputeResourceEntity(); // Core fields - entity.setComputeResourceId(dto.getComputeResourceId()); + entity.setResourceId(dto.getComputeResourceId()); entity.setHostName(dto.getHostName()); entity.setEnabled(dto.isEnabled() ? (short) 1 : (short) 0); entity.setCpusPerNode(dto.getCpuCores()); - entity.setMaxMemoryPerNode(dto.getMemoryGB()); + entity.setMaxMemoryNode(dto.getMemoryGB()); // Embed UI fields into description as JSON String descriptionWithUIFields = encodeComputeUIFieldsIntoDescription(dto); diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java index bc562c46ca4..44c0c0e65d5 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java @@ -24,7 +24,7 @@ import jakarta.validation.Valid; import java.util.List; import org.apache.airavata.research.service.dto.ComputeResourceDTO; -import org.apache.airavata.research.service.handler.LocalComputeResourceHandler; +import org.apache.airavata.research.service.handler.ComputeResourceHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -49,7 +49,7 @@ public class ComputeResourceController { private static final Logger LOGGER = LoggerFactory.getLogger(ComputeResourceController.class); @Autowired - private LocalComputeResourceHandler localComputeResourceHandler; + private ComputeResourceHandler computeResourceHandler; @Operation(summary = "Get all public compute resources") @GetMapping("/public") @@ -62,9 +62,9 @@ public ResponseEntity> getComputeResources( List resources; if (nameSearch != null && !nameSearch.trim().isEmpty()) { - resources = localComputeResourceHandler.searchComputeResources(nameSearch); + resources = computeResourceHandler.searchComputeResources(nameSearch); } else { - resources = localComputeResourceHandler.getAllComputeResources(); + resources = computeResourceHandler.getAllComputeResources(); } LOGGER.info("Found {} compute resources", resources.size()); @@ -81,7 +81,7 @@ public ResponseEntity getComputeResourceById(@PathVariable(" LOGGER.info("Getting compute resource by ID: {}", id); try { - ComputeResourceDTO resource = localComputeResourceHandler.getComputeResource(id); + ComputeResourceDTO resource = computeResourceHandler.getComputeResource(id); return ResponseEntity.ok(resource); } catch (RuntimeException e) { if (e.getMessage().contains("not found")) { @@ -118,7 +118,7 @@ public ResponseEntity createComputeResource(@Valid @RequestBody ComputeResour computeResourceDTO.setMemoryGB(1); // Default to 1 GB } - ComputeResourceDTO savedResource = localComputeResourceHandler.createComputeResource(computeResourceDTO); + ComputeResourceDTO savedResource = computeResourceHandler.createComputeResource(computeResourceDTO); LOGGER.info("Created compute resource with ID: {}", savedResource.getComputeResourceId()); return ResponseEntity.status(HttpStatus.CREATED).body(savedResource); @@ -145,7 +145,7 @@ public ResponseEntity updateComputeResource(@PathVariable("id") String id, @V } try { - ComputeResourceDTO updatedResource = localComputeResourceHandler.updateComputeResource(id, computeResourceDTO); + ComputeResourceDTO updatedResource = computeResourceHandler.updateComputeResource(id, computeResourceDTO); LOGGER.info("Successfully updated compute resource with ID: {}", id); return ResponseEntity.ok(updatedResource); @@ -162,7 +162,7 @@ public ResponseEntity deleteComputeResource(@PathVariable("id") String id) { LOGGER.info("Deleting compute resource with ID: {}", id); try { - localComputeResourceHandler.deleteComputeResource(id); + computeResourceHandler.deleteComputeResource(id); LOGGER.info("Successfully deleted compute resource with ID: {}", id); return ResponseEntity.ok().body("Compute resource deleted successfully"); } catch (Exception e) { @@ -180,7 +180,7 @@ public ResponseEntity> searchComputeResources( LOGGER.info("Searching compute resources with keyword: {}", keyword); try { - List resources = localComputeResourceHandler.searchComputeResources(keyword); + List resources = computeResourceHandler.searchComputeResources(keyword); LOGGER.info("Found {} compute resources matching keyword: {}", resources.size(), keyword); return ResponseEntity.ok(resources); } catch (Exception e) { @@ -195,7 +195,7 @@ public ResponseEntity starComputeResource(@PathVariable("id") String id LOGGER.info("Toggling star for compute resource with ID: {}", id); try { - if (localComputeResourceHandler.existsComputeResource(id)) { + if (computeResourceHandler.existsComputeResource(id)) { // TODO: Implement proper v1 ResourceStar system integration // For now, return simple toggle response LOGGER.info("Star toggle requested for compute resource: {} (simplified implementation)", id); @@ -216,7 +216,7 @@ public ResponseEntity checkComputeResourceStarred(@PathVariable("id") S LOGGER.info("Checking if compute resource is starred: {}", id); try { - if (localComputeResourceHandler.existsComputeResource(id)) { + if (computeResourceHandler.existsComputeResource(id)) { // TODO: Implement proper v1 ResourceStar system integration LOGGER.info("Star status check for compute resource: {} (simplified implementation)", id); return ResponseEntity.ok(false); @@ -236,7 +236,7 @@ public ResponseEntity getComputeResourceStarCount(@PathVariable("id") S LOGGER.info("Getting star count for compute resource: {}", id); try { - if (localComputeResourceHandler.existsComputeResource(id)) { + if (computeResourceHandler.existsComputeResource(id)) { // TODO: Implement proper v1 ResourceStar system integration return ResponseEntity.ok(0); } else { diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java index bc38371cd27..96973a5ca8c 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java @@ -24,7 +24,7 @@ import jakarta.validation.Valid; import java.util.List; import org.apache.airavata.research.service.dto.StorageResourceDTO; -import org.apache.airavata.research.service.handler.LocalStorageResourceHandler; +import org.apache.airavata.research.service.handler.StorageResourceHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -49,7 +49,7 @@ public class StorageResourceController { private static final Logger LOGGER = LoggerFactory.getLogger(StorageResourceController.class); @Autowired - private LocalStorageResourceHandler localStorageResourceHandler; + private StorageResourceHandler storageResourceHandler; @Operation(summary = "Get all public storage resources") @GetMapping("/public") @@ -62,9 +62,9 @@ public ResponseEntity> getStorageResources( List resources; if (nameSearch != null && !nameSearch.trim().isEmpty()) { - resources = localStorageResourceHandler.searchStorageResources(nameSearch); + resources = storageResourceHandler.searchStorageResources(nameSearch); } else { - resources = localStorageResourceHandler.getAllStorageResources(); + resources = storageResourceHandler.getAllStorageResources(); } LOGGER.info("Found {} storage resources", resources.size()); @@ -81,7 +81,7 @@ public ResponseEntity getStorageResourceById(@PathVariable(" LOGGER.info("Getting storage resource by ID: {}", id); try { - StorageResourceDTO resource = localStorageResourceHandler.getStorageResource(id); + StorageResourceDTO resource = storageResourceHandler.getStorageResource(id); return ResponseEntity.ok(resource); } catch (RuntimeException e) { if (e.getMessage().contains("not found")) { @@ -114,7 +114,7 @@ public ResponseEntity createStorageResource(@Valid @RequestBody StorageResour storageResourceDTO.setCapacityTB(1L); // Default to 1 TB } - StorageResourceDTO savedResource = localStorageResourceHandler.createStorageResource(storageResourceDTO); + StorageResourceDTO savedResource = storageResourceHandler.createStorageResource(storageResourceDTO); LOGGER.info("Created storage resource with ID: {}", savedResource.getStorageResourceId()); return ResponseEntity.status(HttpStatus.CREATED).body(savedResource); @@ -141,7 +141,7 @@ public ResponseEntity updateStorageResource(@PathVariable("id") String id, @V } try { - StorageResourceDTO updatedResource = localStorageResourceHandler.updateStorageResource(id, storageResourceDTO); + StorageResourceDTO updatedResource = storageResourceHandler.updateStorageResource(id, storageResourceDTO); LOGGER.info("Successfully updated storage resource with ID: {}", id); return ResponseEntity.ok(updatedResource); @@ -158,7 +158,7 @@ public ResponseEntity deleteStorageResource(@PathVariable("id") String id) { LOGGER.info("Deleting storage resource with ID: {}", id); try { - localStorageResourceHandler.deleteStorageResource(id); + storageResourceHandler.deleteStorageResource(id); LOGGER.info("Successfully deleted storage resource with ID: {}", id); return ResponseEntity.ok().body("Storage resource deleted successfully"); } catch (Exception e) { @@ -176,7 +176,7 @@ public ResponseEntity> searchStorageResources( LOGGER.info("Searching storage resources with keyword: {}", keyword); try { - List resources = localStorageResourceHandler.searchStorageResources(keyword); + List resources = storageResourceHandler.searchStorageResources(keyword); LOGGER.info("Found {} storage resources matching keyword: {}", resources.size(), keyword); return ResponseEntity.ok(resources); } catch (Exception e) { @@ -193,7 +193,7 @@ public ResponseEntity> getStorageResourcesByType( LOGGER.info("Getting storage resources by type: {}", storageType); try { - List resources = localStorageResourceHandler.getStorageResourcesByType(storageType); + List resources = storageResourceHandler.getStorageResourcesByType(storageType); LOGGER.info("Found {} storage resources of type: {}", resources.size(), storageType); return ResponseEntity.ok(resources); } catch (Exception e) { @@ -208,7 +208,7 @@ public ResponseEntity starStorageResource(@PathVariable("id") String id LOGGER.info("Toggling star for storage resource with ID: {}", id); try { - if (localStorageResourceHandler.existsStorageResource(id)) { + if (storageResourceHandler.existsStorageResource(id)) { // TODO: Implement proper v1 ResourceStar system integration // For now, return simple toggle response LOGGER.info("Star toggle requested for storage resource: {} (simplified implementation)", id); @@ -229,7 +229,7 @@ public ResponseEntity checkStorageResourceStarred(@PathVariable("id") S LOGGER.info("Checking if storage resource is starred: {}", id); try { - if (localStorageResourceHandler.existsStorageResource(id)) { + if (storageResourceHandler.existsStorageResource(id)) { // TODO: Implement proper v1 ResourceStar system integration LOGGER.info("Star status check for storage resource: {} (simplified implementation)", id); return ResponseEntity.ok(false); @@ -249,7 +249,7 @@ public ResponseEntity getStorageResourceStarCount(@PathVariable("id") S LOGGER.info("Getting star count for storage resource: {}", id); try { - if (localStorageResourceHandler.existsStorageResource(id)) { + if (storageResourceHandler.existsStorageResource(id)) { // TODO: Implement proper v1 ResourceStar system integration return ResponseEntity.ok(0); } else { diff --git a/modules/research-framework/research-service/src/main/resources/application.yml b/modules/research-framework/research-service/src/main/resources/application.yml index bb6923287eb..7a4c1798c18 100644 --- a/modules/research-framework/research-service/src/main/resources/application.yml +++ b/modules/research-framework/research-service/src/main/resources/application.yml @@ -22,6 +22,14 @@ server: port: 8080 address: 0.0.0.0 +app: + datasource: + app-catalog: + url: jdbc:mariadb://airavata.host:13306/app_catalog + username: airavata + password: 123456 + driver-class-name: org.mariadb.jdbc.Driver + airavata: research-hub: url: http://airavata.host:20000 @@ -57,7 +65,11 @@ spring: leak-detection-threshold: 20000 jpa: hibernate: - ddl-auto: create-drop + ddl-auto: none # Don't modify existing app_catalog schema + properties: + hibernate: + dialect: org.hibernate.dialect.MariaDBDialect + format_sql: true open-in-view: false show-sql: true h2: From 6288c148ad745784207fb61de9692bd055eca911 Mon Sep 17 00:00:00 2001 From: Krish Katariya Date: Sat, 2 Aug 2025 01:28:57 -0700 Subject: [PATCH 11/17] Updating v1 API Dev Data Initializer --- .../service/config/DevDataInitializer.java | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java index 55a5a4d4ee5..c864edbe68f 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java @@ -22,8 +22,11 @@ import java.util.HashSet; import java.util.Set; import org.apache.airavata.research.service.enums.PrivacyEnum; +import org.apache.airavata.research.service.enums.StateEnum; import org.apache.airavata.research.service.enums.StatusEnum; import org.apache.airavata.research.service.model.entity.DatasetResource; +import org.apache.airavata.research.service.model.entity.ModelResource; +import org.apache.airavata.research.service.model.entity.NotebookResource; import org.apache.airavata.research.service.model.entity.Project; import org.apache.airavata.research.service.model.entity.RepositoryResource; import org.apache.airavata.research.service.model.entity.Tag; @@ -36,7 +39,7 @@ import org.springframework.stereotype.Component; @Component -@Profile("dev-local") +@Profile("dev") public class DevDataInitializer implements CommandLineRunner { private final ProjectRepository projectRepository; @@ -80,6 +83,7 @@ private void createProject( repo.setHeaderImage("header_image.png"); repo.setRepositoryUrl(repoUrl); repo.setStatus(StatusEnum.VERIFIED); + repo.setState(StateEnum.ACTIVE); repo.setPrivacy(PrivacyEnum.PUBLIC); repo.setTags(tagSet); repo.setAuthors(authors); @@ -91,16 +95,43 @@ private void createProject( dataset.setHeaderImage("header_image.png"); dataset.setDatasetUrl(datasetUrl); dataset.setStatus(StatusEnum.VERIFIED); + dataset.setState(StateEnum.ACTIVE); dataset.setPrivacy(PrivacyEnum.PUBLIC); dataset.setTags(tagSet); dataset.setAuthors(authors); dataset = resourceRepository.save(dataset); + ModelResource model = new ModelResource(); + model.setName(name + " - ML Model"); + model.setDescription("Machine learning model for " + description); + model.setHeaderImage("header_image.png"); + model.setApplicationInterfaceId("app-" + datasetUrl); + model.setVersion("1.0"); + model.setStatus(StatusEnum.VERIFIED); + model.setState(StateEnum.ACTIVE); + model.setPrivacy(PrivacyEnum.PUBLIC); + model.setTags(tagSet); + model.setAuthors(authors); + model = resourceRepository.save(model); + + NotebookResource notebook = new NotebookResource(); + notebook.setName(name + " - Analysis Notebook"); + notebook.setDescription("Jupyter notebook for " + description); + notebook.setHeaderImage("header_image.png"); + notebook.setNotebookPath(datasetUrl + ".ipynb"); + notebook.setStatus(StatusEnum.VERIFIED); + notebook.setState(StateEnum.ACTIVE); + notebook.setPrivacy(PrivacyEnum.PUBLIC); + notebook.setTags(tagSet); + notebook.setAuthors(authors); + notebook = resourceRepository.save(notebook); + Project project = new Project(); project.setRepositoryResource(repo); project.getDatasetResources().add(dataset); project.setName(name); project.setOwnerId(user); + project.setState(StateEnum.ACTIVE); projectRepository.save(project); System.out.println("Initialized Project with id: " + project.getId()); From 736e6f9f5a4d791368f5280afc6fe31005efffb7 Mon Sep 17 00:00:00 2001 From: Krish Katariya Date: Sat, 2 Aug 2025 17:02:40 -0700 Subject: [PATCH 12/17] v2 API DTO Modifications --- .gitignore | 1 + .../research-service/README.md | 352 +++++++++++++++++- ...01-increase-description-column-lengths.sql | 25 ++ .../research-service/pom.xml | 32 ++ .../config/ApiKeyAuthenticationFilter.java | 65 ++++ .../config/ApiKeyAuthenticationToken.java | 47 +++ .../service/config/AuthzTokenFilter.java | 72 +++- .../CustomAuthenticationEntryPoint.java | 70 ++++ .../service/config/SecurityConfig.java | 122 ++++++ .../service/controller/DevAuthController.java | 96 +++++ .../service/entity/ComputeResourceEntity.java | 2 +- .../service/entity/StorageResourceEntity.java | 2 +- .../service/service/UserContextService.java | 95 +++++ .../research/service/util/DTOConverter.java | 140 ++++++- .../service/v2/controller/CodeController.java | 9 +- .../controller/ComputeResourceController.java | 108 +++++- .../controller/StorageResourceController.java | 60 ++- .../src/main/resources/application.yml | 16 + 18 files changed, 1269 insertions(+), 45 deletions(-) create mode 100644 modules/research-framework/research-service/database-migrations/001-increase-description-column-lengths.sql create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/ApiKeyAuthenticationFilter.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/ApiKeyAuthenticationToken.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/CustomAuthenticationEntryPoint.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/SecurityConfig.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/DevAuthController.java create mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/service/UserContextService.java diff --git a/.gitignore b/.gitignore index 347d0a12892..c3b5a993bc2 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,4 @@ logs/ **/generated-sources/ /distribution /vault* +/airavata-api/distribution/ diff --git a/modules/research-framework/research-service/README.md b/modules/research-framework/research-service/README.md index 79c587882c0..32fc72d1e98 100644 --- a/modules/research-framework/research-service/README.md +++ b/modules/research-framework/research-service/README.md @@ -17,24 +17,360 @@ under the License. --> -# Research Service Application +# Apache Airavata Research Service -This Spring Boot application supports different profiles for running in production vs development mode. In production mode, a security filter enforces authentication. In development mode, the security filter is bypassed for easier local testing. +A comprehensive Spring Boot REST API service for managing research resources, computational infrastructure, and research workflows. This service provides a unified interface for researchers to discover, manage, and utilize computational resources and research artifacts. -## Running in Development Mode +## 🏗️ Architecture Overview -### Using Maven +The Research Service employs a **dual database architecture** designed to separate research data from infrastructure management: + +- **H2 Database (In-Memory)**: Manages v1 research resources (Projects, Datasets, Models, Notebooks, Repositories) +- **MariaDB Database**: Manages v2 infrastructure resources (Compute Resources, Storage Resources) +- **RESTful API**: Comprehensive v1 and v2 endpoints with different authentication requirements +- **Multi-Profile Configuration**: Supports development and production environments + +### Key Components + +- **Layered Architecture**: Controllers → Handlers/Services → Repositories → Entities +- **Authentication**: JWT + API Key dual authentication system +- **Data Conversion**: Advanced DTO-Entity mapping with JSON serialization for UI fields +- **Auto-Initialization**: Automated sample data seeding for development + +## 🚀 Quick Start + +### 1. Prerequisites + +- Java 11+ +- Maven 3.6+ +- Docker & Docker Compose +- MariaDB client (for migrations) + +### 2. Start Airavata Database Stack ```bash +cd "/Users/krishkatariya/dev/Professional Work/Google Summer of Code/airavata" + +# Add hostname mapping (one-time setup) +echo "127.0.0.1 airavata.host" | sudo tee -a /etc/hosts + +# Start MariaDB + Adminer web interface +docker-compose -f .devcontainer/docker-compose.yml up db adminer +``` + +**Database Access:** +- **Host**: `airavata.host:13306` +- **Username**: `airavata` +- **Password**: `123456` +- **Database**: `app_catalog` +- **Web Admin**: http://localhost:18088 + +### 3. Apply Database Migrations + +```bash +cd airavata/modules/research-framework/research-service + +# Run column length migration (required for enhanced JSON storage) +mysql -h airavata.host -P 13306 -u airavata -p123456 app_catalog < database-migrations/001-increase-description-column-lengths.sql +``` + +### 4. Start Research Service + +```bash +# Development mode (includes sample data) mvn spring-boot:run -Dspring-boot.run.profiles=dev + +# Production mode +mvn spring-boot:run +``` + +### 5. Access the Service + +- **API Base URL**: http://localhost:8080 +- **Swagger UI**: http://localhost:8080/swagger-ui.html +- **H2 Console**: http://localhost:8080/h2-console +- **Health Check**: http://localhost:8080/actuator/health + +## 📊 Database Architecture + +### Dual Database System + +#### H2 Database (v1 Resources) +- **Purpose**: Research-focused entities and sample data +- **Location**: In-memory (`jdbc:h2:mem:testdb`) +- **Entities**: `Resource`, `Project`, `ResourceStar`, `Tag`, `Session` +- **Resource Types**: `DatasetResource`, `ModelResource`, `NotebookResource`, `RepositoryResource` +- **Sample Data**: 39+ resources across neuroscience research projects + +#### MariaDB Database (v2 Infrastructure) +- **Purpose**: Production infrastructure and computational resources +- **Location**: `airavata.host:13306/app_catalog` +- **Entities**: `ComputeResourceEntity`, `StorageResourceEntity`, `Code` +- **Resource Types**: HPC clusters, storage systems, research codes +- **Sample Data**: 12+ infrastructure resources + +### Data Initializers + +- **`DatasetInitializer`**: Creates 9 research datasets (all profiles) +- **`DevDataInitializer`**: Creates 10 neuroscience projects with full resource sets (dev profile only) +- **`V2DataInitializer`**: Creates 11 code resources and samples (all profiles) + +## 🔐 Authentication + +### JWT Authentication (Users) +```bash +# Headers for authenticated requests +Authorization: Bearer +X-Claims: {"userName":"user@domain.com","gatewayID":"default"} +``` + +### API Key Authentication (Services) +```bash +# Headers for service requests +X-API-Key: dev-research-api-key-12345 +``` + +### Development Token Generation +```bash +# Generate test JWT +curl -X POST http://localhost:8080/api/dev/auth/token \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com","name":"Test User"}' +``` + +## 🌐 API Endpoints + +### V1 API - Research Resources (H2 Database) + +#### Projects (`/api/v1/rf/projects`) +- `GET /` - List all projects +- `GET /{ownerId}` - Get projects by owner +- `POST /` - Create new project +- `DELETE /{projectId}` - Delete project + +#### Resources (`/api/v1/rf/resources`) +- `GET /public` - List all public resources (with pagination, filtering) +- `GET /public/{id}` - Get resource by ID +- `GET /public/tags/all` - Get all tags sorted by popularity +- `GET /search` - Search resources by type and name +- `POST /dataset` - Create dataset resource +- `POST /notebook` - Create notebook resource +- `POST /repository` - Create repository resource +- `POST /model` - Create model resource +- `PATCH /repository` - Modify repository resource +- `DELETE /{id}` - Delete resource +- `POST /{id}/star` - Star/unstar resource +- `GET /{id}/star` - Check star status +- `GET /resources/{id}/count` - Get star count +- `GET /{userId}/stars` - Get user's starred resources + +#### Sessions (`/api/v1/rf/sessions`) +- `GET /` - List user sessions (with status filtering) +- `PATCH /{sessionId}` - Update session status +- `DELETE /{sessionId}` - Delete session + +#### Research Hub (`/api/v1/rf/hub`) +- `GET /start/project/{projectId}` - Start hub session for project +- `GET /resume/session/{sessionId}` - Resume existing session + +### V2 API - Infrastructure Resources (MariaDB) + +#### Codes (`/api/v2/rf/codes`) 🔒 +- `GET /` - List codes (with pagination, filtering) +- `GET /{id}` - Get code by ID +- `POST /` - Create code resource +- `PUT /{id}` - Update code resource +- `DELETE /{id}` - Delete code resource +- `GET /search` - Search by keyword +- `GET /type/{codeType}` - Filter by code type +- `GET /language/{language}` - Filter by programming language +- `GET /framework/{framework}` - Filter by framework +- `GET /tag/{tag}` - Filter by tag +- `GET /author/{author}` - Filter by author +- `GET /top-starred` - Get most starred codes +- `GET /recent` - Get recent codes +- `POST /{id}/star` - Star/unstar code +- `GET /{id}/star` - Check star status +- `GET /{id}/stars/count` - Get star count +- `GET /starred` - Get starred codes + +#### Compute Resources (`/api/v2/rf/compute-resources`) 🔒 +- `GET /` - List compute resources (with name search) +- `GET /{id}` - Get compute resource by ID +- `POST /` - Create compute resource +- `PUT /{id}` - Update compute resource +- `DELETE /{id}` - Delete compute resource +- `GET /search` - Search by keyword +- `POST /{id}/star` - Star/unstar resource +- `GET /{id}/star` - Check star status +- `GET /{id}/stars/count` - Get star count +- `GET /starred` - Get starred resources + +#### Storage Resources (`/api/v2/rf/storage-resources`) 🔒 +- `GET /` - List storage resources (with name search) +- `GET /{id}` - Get storage resource by ID +- `POST /` - Create storage resource +- `PUT /{id}` - Update storage resource +- `DELETE /{id}` - Delete storage resource +- `GET /search` - Search by keyword +- `GET /type/{storageType}` - Filter by storage type +- `POST /{id}/star` - Star/unstar resource +- `GET /{id}/star` - Check star status +- `GET /{id}/stars/count` - Get star count +- `GET /starred` - Get starred resources + +### Development APIs (`/api/dev`) +- `POST /auth/token` - Generate test JWT token +- `POST /auth/api-key-info` - Get API key information + +🔒 = Requires authentication (JWT or API Key) + +## 📝 Sample API Requests + +### Create Compute Resource +```json +POST /api/v2/rf/compute-resources/ +Headers: X-API-Key: dev-research-api-key-12345 + +{ + "name": "Titan Supercomputer", + "resourceDescription": "A powerful HPC cluster for scientific simulations", + "hostName": "titan.supercluster.edu", + "computeType": "HPC", + "cpuCores": 299008, + "memoryGB": 710000, + "operatingSystem": "Cray Linux Environment", + "hostAliases": ["titan-login1.supercluster.edu"], + "ipAddresses": ["128.219.10.1"], + "sshUsername": "user123", + "sshPort": 22, + "authenticationMethod": "SSH_KEY", + "workingDirectory": "/lustre/home/user123", + "schedulerType": "SLURM", + "dataMovementProtocol": "SCP", + "queues": [ + { + "queueName": "default", + "maxNodes": 100, + "maxProcessors": 2048, + "maxRunTime": 7200 + } + ], + "enabled": true +} ``` -### Using IntelliJ IDEA +### Create Storage Resource (S3) +```json +POST /api/v2/rf/storage-resources/ +Headers: X-API-Key: dev-research-api-key-12345 + +{ + "name": "S3 Research Storage", + "hostName": "s3.amazonaws.com", + "storageResourceDescription": "AWS S3 bucket for research data", + "storageType": "S3", + "capacityTB": 1000, + "accessProtocol": "HTTPS", + "endpoint": "https://s3.amazonaws.com", + "supportsEncryption": true, + "supportsVersioning": true, + "bucketName": "my-research-bucket", + "accessKey": "AKIAIOSFODNN7EXAMPLE", + "secretKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "enabled": true +} +``` -1. Go to Run > Edit Configurations. +## 🧪 Development Configuration + +### IntelliJ IDEA Setup +1. Go to **Run > Edit Configurations** 2. Select your Spring Boot run configuration -3. In the Program arguments field, add: +3. In **Program arguments**, add: `--spring.profiles.active=dev` + +### Environment Variables +```bash +# Override database configuration +export SPRING_DATASOURCE_URL=jdbc:mariadb://custom-host:3306/app_catalog +export SPRING_DATASOURCE_USERNAME=custom_user +export SPRING_DATASOURCE_PASSWORD=custom_password + +# Override authentication +export RESEARCH_AUTH_DEV_API_KEY=your-custom-api-key +``` + +### Profile-Specific Behavior +- **Default Profile**: Production-ready with minimal sample data +- **Dev Profile**: Includes comprehensive sample data and relaxed security + +## 🔧 Key Features +### Enhanced Data Management +- **Field Preservation**: Complete round-trip data integrity for complex objects +- **JSON Serialization**: Advanced UI field embedding in database description columns +- **Validation**: Comprehensive Jakarta Bean Validation with detailed error messages + +### Search & Discovery +- **Multi-faceted Search**: Search by keyword, type, tags, author, language, framework +- **Pagination**: Configurable page size and sorting +- **Popularity Metrics**: Star counts and trending algorithms + +### Integration Ready +- **CORS Support**: Configurable for frontend integration +- **OpenAPI Documentation**: Auto-generated Swagger documentation +- **Health Monitoring**: Spring Boot Actuator endpoints + +## 🐛 Troubleshooting + +### Common Issues + +**Database Connection Failed** ```bash ---spring.profiles.active=dev +# Verify database is running +docker ps | grep mariadb + +# Check hostname mapping +ping airavata.host + +# Test database connection +mysql -h airavata.host -P 13306 -u airavata -p123456 -e "SHOW DATABASES;" ``` + +**Column Length Errors** +```bash +# Apply the database migration +mysql -h airavata.host -P 13306 -u airavata -p123456 app_catalog < database-migrations/001-increase-description-column-lengths.sql +``` + +**Authentication Issues** +- Verify JWT token format and claims +- Check API key matches configuration +- Ensure proper headers are set + +### Logging +```bash +# Enable debug logging +export LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_SECURITY=DEBUG +``` + +## 📚 Development Resources + +- **API Documentation**: http://localhost:8080/swagger-ui.html +- **Database Console**: http://localhost:8080/h2-console (H2 only) +- **Source Code**: `src/main/java/org/apache/airavata/research/service/` +- **Configuration**: `src/main/resources/application.yml` +- **Migrations**: `database-migrations/` + +## 🤝 Contributing + +When making significant changes to the Research Service: + +1. **Update Documentation**: Keep this README current with new endpoints, configuration changes, and architectural updates +2. **Run Tests**: Ensure all existing functionality remains intact +3. **Database Migrations**: Create numbered migration scripts in `database-migrations/` for schema changes +4. **API Documentation**: Update Swagger annotations for new endpoints + +--- + +**Apache Airavata Research Service** - Empowering scientific discovery through unified research resource management. \ No newline at end of file diff --git a/modules/research-framework/research-service/database-migrations/001-increase-description-column-lengths.sql b/modules/research-framework/research-service/database-migrations/001-increase-description-column-lengths.sql new file mode 100644 index 00000000000..42cf382922c --- /dev/null +++ b/modules/research-framework/research-service/database-migrations/001-increase-description-column-lengths.sql @@ -0,0 +1,25 @@ +-- Migration: Increase Description Column Lengths +-- Date: 2025-01-02 +-- Issue: "Data too long for column" error when serializing UI fields to JSON +-- +-- This migration increases the column lengths for RESOURCE_DESCRIPTION and DESCRIPTION +-- columns to accommodate enhanced JSON serialization of UI fields including: +-- - name, hostAliases, ipAddresses, queues (compute resources) +-- - name and additional UI fields (storage resources) + +-- Increase RESOURCE_DESCRIPTION column length in COMPUTE_RESOURCE table +ALTER TABLE COMPUTE_RESOURCE MODIFY COLUMN RESOURCE_DESCRIPTION VARCHAR(2048); + +-- Increase DESCRIPTION column length in STORAGE_RESOURCE table +ALTER TABLE STORAGE_RESOURCE MODIFY COLUMN DESCRIPTION VARCHAR(2048); + +-- Verification query (optional - run to confirm changes) +-- SELECT +-- TABLE_NAME, +-- COLUMN_NAME, +-- DATA_TYPE, +-- CHARACTER_MAXIMUM_LENGTH +-- FROM INFORMATION_SCHEMA.COLUMNS +-- WHERE TABLE_SCHEMA = 'app_catalog' +-- AND TABLE_NAME IN ('COMPUTE_RESOURCE', 'STORAGE_RESOURCE') +-- AND COLUMN_NAME IN ('RESOURCE_DESCRIPTION', 'DESCRIPTION'); \ No newline at end of file diff --git a/modules/research-framework/research-service/pom.xml b/modules/research-framework/research-service/pom.xml index 142e9b3dd2a..0b033c3dac8 100644 --- a/modules/research-framework/research-service/pom.xml +++ b/modules/research-framework/research-service/pom.xml @@ -178,6 +178,38 @@ under the License. + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + ch.qos.logback + logback-classic + + + ch.qos.logback + logback-core + + + + + + + org.springframework.security + spring-security-oauth2-jose + + + ch.qos.logback + logback-classic + + + ch.qos.logback + logback-core + + + diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/ApiKeyAuthenticationFilter.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/ApiKeyAuthenticationFilter.java new file mode 100644 index 00000000000..399128dc0dd --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/ApiKeyAuthenticationFilter.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.airavata.research.service.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Arrays; + +@Component +public class ApiKeyAuthenticationFilter extends OncePerRequestFilter { + + private final String devApiKey; + private static final String API_KEY_HEADER = "X-API-Key"; + + public ApiKeyAuthenticationFilter(@Value("${research.auth.dev-api-key}") String devApiKey) { + this.devApiKey = devApiKey; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + String apiKey = request.getHeader(API_KEY_HEADER); + + if (apiKey != null && devApiKey.equals(apiKey)) { + // Create API key authentication + ApiKeyAuthenticationToken authentication = new ApiKeyAuthenticationToken( + "api-key-user", + null, + Arrays.asList(new SimpleGrantedAuthority("ROLE_API_USER")) + ); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/ApiKeyAuthenticationToken.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/ApiKeyAuthenticationToken.java new file mode 100644 index 00000000000..b2d5fe08a08 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/ApiKeyAuthenticationToken.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.airavata.research.service.config; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { + private final Object principal; + private Object credentials; + + public ApiKeyAuthenticationToken(Object principal, Object credentials, Collection authorities) { + super(authorities); + this.principal = principal; + this.credentials = credentials; + setAuthenticated(true); + } + + @Override + public Object getCredentials() { + return credentials; + } + + @Override + public Object getPrincipal() { + return principal; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java index fa556255473..3c50339af34 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java @@ -26,6 +26,8 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Base64; +import java.util.HashMap; import java.util.Map; import org.apache.airavata.model.security.AuthzToken; import org.apache.airavata.model.user.UserProfile; @@ -84,11 +86,21 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse return; } - if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ") && xClaimsHeader != null) { + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { try { String accessToken = authorizationHeader.substring(7); // Remove "Bearer " prefix - ObjectMapper objectMapper = new ObjectMapper(); - Map claimsMap = objectMapper.readValue(xClaimsHeader, new TypeReference<>() {}); + Map claimsMap; + + // Primary: Use X-Claims header if available (frontend compatibility) + if (xClaimsHeader != null) { + ObjectMapper objectMapper = new ObjectMapper(); + claimsMap = objectMapper.readValue(xClaimsHeader, new TypeReference<>() {}); + LOGGER.debug("Using claims from X-Claims header"); + } else { + // Fallback: Extract claims from JWT payload for pure OAuth2/OIDC clients + claimsMap = extractClaimsFromJWT(accessToken); + LOGGER.debug("Using claims extracted from JWT payload"); + } AuthzToken authzToken = new AuthzToken(); authzToken.setAccessToken(accessToken); @@ -111,6 +123,60 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } } + /** + * Extract claims from JWT payload as fallback when X-Claims header is not present + * This enables pure OAuth2/OIDC clients to work without custom headers + */ + private Map extractClaimsFromJWT(String jwt) { + try { + // Split JWT into parts (header.payload.signature) + String[] parts = jwt.split("\\."); + if (parts.length != 3) { + throw new IllegalArgumentException("Invalid JWT format"); + } + + // Decode the payload (second part) + String payload = new String(Base64.getUrlDecoder().decode(parts[1])); + + // Parse JSON payload + ObjectMapper objectMapper = new ObjectMapper(); + Map jwtClaims = objectMapper.readValue(payload, new TypeReference<>() {}); + + // Convert to string map and extract required claims + Map claimsMap = new HashMap<>(); + + // Map standard OIDC claims to Airavata claims + String email = getClaimValue(jwtClaims, "email", "preferred_username", "sub"); + if (email != null) { + claimsMap.put(USERNAME_CLAIM, email); + } + + // Default gateway for JWT-only clients + claimsMap.put(GATEWAY_CLAIM, "default"); + + LOGGER.debug("Extracted claims from JWT: userName={}, gatewayID={}", + claimsMap.get(USERNAME_CLAIM), claimsMap.get(GATEWAY_CLAIM)); + + return claimsMap; + } catch (Exception e) { + LOGGER.error("Failed to extract claims from JWT", e); + throw new IllegalArgumentException("Invalid JWT token", e); + } + } + + /** + * Get claim value from JWT payload, trying multiple possible claim names + */ + private String getClaimValue(Map jwtClaims, String... claimNames) { + for (String claimName : claimNames) { + Object value = jwtClaims.get(claimName); + if (value != null) { + return value.toString(); + } + } + return null; + } + private static String getClaim(AuthzToken authzToken, String claimId) { return authzToken.getClaimsMap().entrySet().stream() .filter(entry -> entry.getKey().equalsIgnoreCase(claimId)) diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/CustomAuthenticationEntryPoint.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/CustomAuthenticationEntryPoint.java new file mode 100644 index 00000000000..addf98b4427 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/CustomAuthenticationEntryPoint.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.airavata.research.service.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + + Map errorResponse = new HashMap<>(); + errorResponse.put("timestamp", Instant.now().toString()); + errorResponse.put("status", 401); + errorResponse.put("error", "Unauthorized"); + errorResponse.put("message", "Authentication required. Please provide a valid JWT token or X-API-Key header."); + errorResponse.put("path", request.getRequestURI()); + + String authHeader = request.getHeader("Authorization"); + String apiKeyHeader = request.getHeader("X-API-Key"); + + if (authHeader == null && apiKeyHeader == null) { + errorResponse.put("details", "Missing authentication. Include either 'Authorization: Bearer ' or 'X-API-Key: ' header."); + } else if (authHeader != null && !authHeader.startsWith("Bearer ")) { + errorResponse.put("details", "Invalid Authorization header format. Use 'Authorization: Bearer '."); + } else if (apiKeyHeader != null) { + errorResponse.put("details", "Invalid API key provided."); + } else { + errorResponse.put("details", "Invalid or expired authentication token."); + } + + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/SecurityConfig.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/SecurityConfig.java new file mode 100644 index 00000000000..e2aeb74b3e2 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/SecurityConfig.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.airavata.research.service.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class SecurityConfig { + + @Value("${research.auth.jwks-uri:https://auth.dev.cybershuttle.org/.well-known/jwks}") + private String jwksUri; + + @Value("${research.auth.dev-api-key:dev-research-api-key-12345}") + private String devApiKey; + + @Autowired + private CustomAuthenticationEntryPoint authenticationEntryPoint; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .authorizeHttpRequests(authz -> authz + // Public endpoints (only v1 has public endpoints now) + .requestMatchers("/api/v1/rf/*/public/**").permitAll() + .requestMatchers("/api/dev/**").permitAll() // Dev endpoints + .requestMatchers("/actuator/health").permitAll() + + // Protected endpoints (all v2 endpoints require authentication) + .requestMatchers("/api/v1/rf/**", "/api/v2/rf/**").authenticated() + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .decoder(jwtDecoder()) + .jwtAuthenticationConverter(jwtAuthenticationConverter()) + ) + .authenticationEntryPoint(authenticationEntryPoint) + ) + .addFilterBefore(apiKeyFilter(), BearerTokenAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.withJwkSetUri(jwksUri).build(); + } + + @Bean + public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + converter.setJwtGrantedAuthoritiesConverter(jwt -> { + // Extract roles from JWT claims + Collection roles = jwt.getClaimAsStringList("roles"); + if (roles == null) { + roles = Arrays.asList("USER"); // Default role + } + return roles.stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) + .collect(Collectors.toList()); + }); + return converter; + } + + @Bean + public ApiKeyAuthenticationFilter apiKeyFilter() { + return new ApiKeyAuthenticationFilter(devApiKey); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(Arrays.asList("*")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/DevAuthController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/DevAuthController.java new file mode 100644 index 00000000000..4e446c871c9 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/DevAuthController.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.airavata.research.service.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/dev") +@Profile("dev") +@Tag(name = "Development Authentication", description = "Development-only endpoints for testing authentication") +public class DevAuthController { + + private static final Logger LOGGER = LoggerFactory.getLogger(DevAuthController.class); + + @Operation(summary = "Generate test JWT token for development") + @PostMapping("/auth/token") + public ResponseEntity> generateTestToken( + @RequestParam(defaultValue = "test@example.com") String email, + @RequestParam(defaultValue = "default") String gatewayId) { + + LOGGER.info("Generating test token for email: {}, gatewayId: {}", email, gatewayId); + + try { + // For development, we'll create a simple token-like response + // In a real implementation, this would use JwtEncoder to create actual JWTs + + Instant now = Instant.now(); + String fakeToken = "dev-jwt-token-" + now.getEpochSecond() + "-" + email.hashCode(); + + Map response = new HashMap<>(); + response.put("access_token", fakeToken); + response.put("token_type", "Bearer"); + response.put("expires_in", 3600); + response.put("email", email); + response.put("gatewayID", gatewayId); + response.put("roles", Arrays.asList("USER")); + response.put("note", "This is a development-only token for testing purposes"); + + LOGGER.info("Generated test token for user: {}", email); + return ResponseEntity.ok(response); + + } catch (Exception e) { + LOGGER.error("Error generating test token: {}", e.getMessage(), e); + Map errorResponse = new HashMap<>(); + errorResponse.put("error", "token_generation_failed"); + errorResponse.put("error_description", e.getMessage()); + return ResponseEntity.internalServerError().body(errorResponse); + } + } + + @Operation(summary = "Get development API key information") + @PostMapping("/auth/api-key-info") + public ResponseEntity> getApiKeyInfo() { + LOGGER.info("Providing development API key information"); + + Map response = new HashMap<>(); + response.put("api_key_header", "X-API-Key"); + response.put("api_key_value", "dev-research-api-key-12345"); + response.put("note", "Use this API key in the X-API-Key header for development testing"); + response.put("example_curl", "curl -H \"X-API-Key: dev-research-api-key-12345\" http://localhost:8080/api/v2/rf/compute-resources/"); + + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/ComputeResourceEntity.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/ComputeResourceEntity.java index 8fa1a0f84d5..403c0a738eb 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/ComputeResourceEntity.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/ComputeResourceEntity.java @@ -16,7 +16,7 @@ public class ComputeResourceEntity implements Serializable { @Column(name = "HOST_NAME", nullable = false) private String hostName; - @Column(name = "RESOURCE_DESCRIPTION") + @Column(name = "RESOURCE_DESCRIPTION", length = 2048) private String resourceDescription; @Column(name = "CREATION_TIME", nullable = false) diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/StorageResourceEntity.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/StorageResourceEntity.java index 77b13083905..22c191af7d4 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/StorageResourceEntity.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/StorageResourceEntity.java @@ -16,7 +16,7 @@ public class StorageResourceEntity implements Serializable { @Column(name = "HOST_NAME", nullable = false) private String hostName; - @Column(name = "DESCRIPTION") + @Column(name = "DESCRIPTION", length = 2048) private String description; @Column(name = "ENABLED") diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/service/UserContextService.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/service/UserContextService.java new file mode 100644 index 00000000000..23439af2657 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/service/UserContextService.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.airavata.research.service.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.airavata.research.service.config.ApiKeyAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.stereotype.Service; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Service +public class UserContextService { + + public String getCurrentUserId() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + + if (auth instanceof JwtAuthenticationToken) { + JwtAuthenticationToken jwtAuth = (JwtAuthenticationToken) auth; + return jwtAuth.getToken().getClaimAsString("email"); + } else if (auth instanceof ApiKeyAuthenticationToken) { + return "api-key-user"; + } + + return "anonymous"; + } + + public String getCurrentGatewayId() { + // Extract from X-Claims header or JWT + ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + String claims = attr.getRequest().getHeader("X-Claims"); + + if (claims != null) { + try { + ObjectMapper mapper = new ObjectMapper(); + JsonNode node = mapper.readTree(claims); + JsonNode gatewayNode = node.get("gatewayID"); + if (gatewayNode != null) { + return gatewayNode.asText(); + } + } catch (Exception e) { + // Fall back to default + } + } + + return "default"; + } + + public boolean isAuthenticated() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + return auth != null && auth.isAuthenticated() && !"anonymous".equals(auth.getName()); + } + + public String getCurrentUserName() { + // Extract from X-Claims header first + ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + String claims = attr.getRequest().getHeader("X-Claims"); + + if (claims != null) { + try { + ObjectMapper mapper = new ObjectMapper(); + JsonNode node = mapper.readTree(claims); + JsonNode userNameNode = node.get("userName"); + if (userNameNode != null) { + return userNameNode.asText(); + } + } catch (Exception e) { + // Fall back to authentication context + } + } + + // Fall back to current user ID + return getCurrentUserId(); + } +} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java index 79a6068853f..421bd1f8b74 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java @@ -61,6 +61,12 @@ public class DTOConverter { private static final String WORKING_DIR_KEY = "workingDirectory"; private static final String SCHEDULER_TYPE_KEY = "schedulerType"; private static final String DATA_MOVEMENT_PROTOCOL_KEY = "dataMovementProtocol"; + + // Additional compute fields that need to be preserved + private static final String NAME_KEY = "name"; + private static final String HOST_ALIASES_KEY = "hostAliases"; + private static final String IP_ADDRESSES_KEY = "ipAddresses"; + private static final String QUEUES_KEY = "queues"; // Storage-specific field names private static final String STORAGE_TYPE_KEY = "storageType"; @@ -488,8 +494,13 @@ public StorageResourceDTO storageEntityToDTO(StorageResourceEntity entity) { Short enabledValue = entity.getEnabled(); dto.setEnabled(enabledValue != null && enabledValue.shortValue() == 1); - // Generate a name from hostname if not available - dto.setName(generateStorageResourceName(entity.getHostName(), entity.getDescription())); + // Extract name from UI fields or generate fallback + String extractedName = extractNameFromStorageDescription(entity.getDescription()); + if (extractedName != null) { + dto.setName(extractedName); + } else { + dto.setName(generateStorageResourceName(entity.getHostName(), entity.getDescription())); + } // Timestamps if (entity.getCreationTime() != null) { @@ -547,8 +558,13 @@ public ComputeResourceDTO computeEntityToDTO(ComputeResourceEntity entity) { dto.setCpuCores(entity.getCpusPerNode()); dto.setMemoryGB(entity.getMaxMemoryNode()); - // Generate a name from hostname if not available - dto.setName(generateComputeResourceName(entity.getHostName(), entity.getResourceDescription())); + // Extract name from UI fields or generate fallback + String extractedName = extractNameFromDescription(entity.getResourceDescription()); + if (extractedName != null) { + dto.setName(extractedName); + } else { + dto.setName(generateComputeResourceName(entity.getHostName(), entity.getResourceDescription())); + } // Timestamps if (entity.getCreationTime() != null) { @@ -560,6 +576,17 @@ public ComputeResourceDTO computeEntityToDTO(ComputeResourceEntity entity) { // Extract UI-specific fields from description JSON extractComputeUIFieldsFromDescription(entity.getResourceDescription(), dto); + + // Initialize empty arrays for fields not stored in database + if (dto.getHostAliases() == null) { + dto.setHostAliases(new ArrayList<>()); + } + if (dto.getIpAddresses() == null) { + dto.setIpAddresses(new ArrayList<>()); + } + if (dto.getQueues() == null) { + dto.setQueues(new ArrayList<>()); + } return dto; } @@ -605,6 +632,9 @@ private void extractStorageUIFieldsFromDescription(String description, StorageRe dto.setAccessProtocol(getStringValue(rootNode, ACCESS_PROTOCOL_KEY)); dto.setSupportsEncryption(getBooleanValue(rootNode, SUPPORTS_ENCRYPTION_KEY)); dto.setSupportsVersioning(getBooleanValue(rootNode, SUPPORTS_VERSIONING_KEY)); + + // Extract preserved fields + dto.setName(getStringValue(rootNode, NAME_KEY)); // S3-specific fields dto.setBucketName(getStringValue(rootNode, BUCKET_NAME_KEY)); @@ -645,6 +675,38 @@ private void extractComputeUIFieldsFromDescription(String description, ComputeRe dto.setOperatingSystem(getStringValue(rootNode, OPERATING_SYSTEM_KEY)); dto.setSchedulerType(getStringValue(rootNode, SCHEDULER_TYPE_KEY)); dto.setDataMovementProtocol(getStringValue(rootNode, DATA_MOVEMENT_PROTOCOL_KEY)); + + // Extract preserved fields + dto.setName(getStringValue(rootNode, NAME_KEY)); + + // Extract arrays + JsonNode hostAliasesNode = rootNode.get(HOST_ALIASES_KEY); + if (hostAliasesNode != null && hostAliasesNode.isArray()) { + List hostAliases = new ArrayList<>(); + hostAliasesNode.forEach(node -> hostAliases.add(node.asText())); + dto.setHostAliases(hostAliases); + } + + JsonNode ipAddressesNode = rootNode.get(IP_ADDRESSES_KEY); + if (ipAddressesNode != null && ipAddressesNode.isArray()) { + List ipAddresses = new ArrayList<>(); + ipAddressesNode.forEach(node -> ipAddresses.add(node.asText())); + dto.setIpAddresses(ipAddresses); + } + + JsonNode queuesNode = rootNode.get(QUEUES_KEY); + if (queuesNode != null && queuesNode.isArray()) { + List queues = new ArrayList<>(); + queuesNode.forEach(queueNode -> { + ComputeResourceQueueDTO queue = new ComputeResourceQueueDTO(); + queue.setQueueName(getStringValue(queueNode, "queueName")); + queue.setMaxNodes(getIntegerValue(queueNode, "maxNodes")); + queue.setMaxProcessors(getIntegerValue(queueNode, "maxProcessors")); + queue.setMaxRunTime(getIntegerValue(queueNode, "maxRunTime")); + queues.add(queue); + }); + dto.setQueues(queues); + } // Clean description (remove UI_FIELDS part) String cleanDescription = description.substring(0, description.indexOf("UI_FIELDS:")).trim(); @@ -676,6 +738,9 @@ private String encodeStorageUIFieldsIntoDescription(StorageResourceDTO dto) { uiFields.put(SUPPORTS_ENCRYPTION_KEY, dto.getSupportsEncryption()); uiFields.put(SUPPORTS_VERSIONING_KEY, dto.getSupportsVersioning()); + // Preserve critical fields + uiFields.put(NAME_KEY, dto.getName()); + // S3-specific fields if (dto.getBucketName() != null) { uiFields.put(BUCKET_NAME_KEY, dto.getBucketName()); @@ -702,13 +767,19 @@ private String encodeStorageUIFieldsIntoDescription(StorageResourceDTO dto) { } String uiFieldsJson = objectMapper.writeValueAsString(uiFields); - description.append("\n\nUI_FIELDS: ").append(uiFieldsJson); + String result = description.toString() + "\n\nUI_FIELDS: " + uiFieldsJson; + + // Check if result exceeds database column limit + if (result.length() > 2000) { + LOGGER.warn("JSON serialization length ({}) may exceed database column limit. Consider running the database migration script.", result.length()); + } + + return result; } catch (Exception e) { LOGGER.warn("Failed to encode storage UI fields", e); + return description.toString(); } - - return description.toString(); } // Helper method to encode compute UI fields into description @@ -728,14 +799,27 @@ private String encodeComputeUIFieldsIntoDescription(ComputeResourceDTO dto) { uiFields.put(SCHEDULER_TYPE_KEY, dto.getSchedulerType()); uiFields.put(DATA_MOVEMENT_PROTOCOL_KEY, dto.getDataMovementProtocol()); + // Preserve critical fields that might be lost + uiFields.put(NAME_KEY, dto.getName()); + uiFields.put(HOST_ALIASES_KEY, dto.getHostAliases()); + uiFields.put(IP_ADDRESSES_KEY, dto.getIpAddresses()); + uiFields.put(QUEUES_KEY, dto.getQueues()); + String uiFieldsJson = objectMapper.writeValueAsString(uiFields); - description.append("\n\nUI_FIELDS: ").append(uiFieldsJson); + String result = description.toString() + "\n\nUI_FIELDS: " + uiFieldsJson; + + // Check if result exceeds database column limit (assume 255 for safety if not migrated) + if (result.length() > 2000) { + LOGGER.warn("JSON serialization length ({}) may exceed database column limit. Consider running the database migration script.", result.length()); + // Could implement compression here if needed, but for now just log the warning + } + + return result; } catch (Exception e) { LOGGER.warn("Failed to encode compute UI fields", e); + return description.toString(); } - - return description.toString(); } /** @@ -782,4 +866,40 @@ private String generateComputeResourceName(String hostName, String description) return "Compute Resource"; } + + /** + * Extract name from description UI fields + */ + private String extractNameFromDescription(String description) { + if (description == null || !description.contains("UI_FIELDS:")) { + return null; + } + + try { + String jsonPart = description.substring(description.indexOf("UI_FIELDS:") + 10).trim(); + JsonNode rootNode = objectMapper.readTree(jsonPart); + return getStringValue(rootNode, NAME_KEY); + } catch (Exception e) { + LOGGER.warn("Failed to extract name from description", e); + return null; + } + } + + /** + * Extract name from storage description UI fields + */ + private String extractNameFromStorageDescription(String description) { + if (description == null || !description.contains("UI_FIELDS:")) { + return null; + } + + try { + String jsonPart = description.substring(description.indexOf("UI_FIELDS:") + 10).trim(); + JsonNode rootNode = objectMapper.readTree(jsonPart); + return getStringValue(rootNode, NAME_KEY); + } catch (Exception e) { + LOGGER.warn("Failed to extract name from storage description", e); + return null; + } + } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java index 61f7c8c265c..b34146afa88 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java @@ -46,6 +46,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.access.prepost.PreAuthorize; @RestController @RequestMapping("/api/v2/rf/codes") @@ -62,8 +63,9 @@ public CodeController(CodeRepository codeRepository) { this.codeRepository = codeRepository; } - @Operation(summary = "Get all public codes with pagination") - @GetMapping("/public") + @Operation(summary = "Get all codes with pagination") + @GetMapping("/") + @PreAuthorize("hasRole('USER') or hasRole('API_USER')") public ResponseEntity> getCodes( @RequestParam(value = "pageNumber", defaultValue = "0") int pageNumber, @RequestParam(value = "pageSize", defaultValue = "10") int pageSize, @@ -88,7 +90,8 @@ public ResponseEntity> getCodes( } @Operation(summary = "Get code by ID") - @GetMapping("/public/{id}") + @GetMapping("/{id}") + @PreAuthorize("hasRole('USER') or hasRole('API_USER')") public ResponseEntity getCodeById(@PathVariable("id") String id) { LOGGER.info("Getting code by ID: {}", id); diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java index 44c0c0e65d5..31a49f98a2a 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java @@ -25,9 +25,11 @@ import java.util.List; import org.apache.airavata.research.service.dto.ComputeResourceDTO; import org.apache.airavata.research.service.handler.ComputeResourceHandler; +import org.apache.airavata.research.service.service.UserContextService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindingResult; @@ -51,8 +53,12 @@ public class ComputeResourceController { @Autowired private ComputeResourceHandler computeResourceHandler; - @Operation(summary = "Get all public compute resources") - @GetMapping("/public") + @Autowired + private UserContextService userContextService; + + @Operation(summary = "Get all compute resources") + @GetMapping("/") + @PreAuthorize("hasRole('USER') or hasRole('API_USER')") public ResponseEntity> getComputeResources( @RequestParam(value = "nameSearch", required = false) String nameSearch) { @@ -76,7 +82,8 @@ public ResponseEntity> getComputeResources( } @Operation(summary = "Get compute resource by ID") - @GetMapping("/public/{id}") + @GetMapping("/{id}") + @PreAuthorize("hasRole('USER') or hasRole('API_USER')") public ResponseEntity getComputeResourceById(@PathVariable("id") String id) { LOGGER.info("Getting compute resource by ID: {}", id); @@ -96,6 +103,7 @@ public ResponseEntity getComputeResourceById(@PathVariable(" @Operation(summary = "Create new compute resource") @PostMapping("/") + @PreAuthorize("hasRole('USER') or hasRole('API_USER')") public ResponseEntity createComputeResource(@Valid @RequestBody ComputeResourceDTO computeResourceDTO, BindingResult bindingResult) { LOGGER.info("Creating new compute resource: {}", computeResourceDTO.getHostName()); @@ -109,17 +117,17 @@ public ResponseEntity createComputeResource(@Valid @RequestBody ComputeResour return ResponseEntity.badRequest().body("Validation failed: " + errorMessage); } + // Set intelligent defaults for fields not provided by UI + setDefaultValues(computeResourceDTO); + try { - // Set default values for fields that might be null - if (computeResourceDTO.getCpuCores() == null) { - computeResourceDTO.setCpuCores(1); // Default to 1 core - } - if (computeResourceDTO.getMemoryGB() == null) { - computeResourceDTO.setMemoryGB(1); // Default to 1 GB - } + + // Set creator from authenticated user + String currentUser = userContextService.getCurrentUserId(); + // Note: ComputeResourceDTO would need a createdBy field to store this ComputeResourceDTO savedResource = computeResourceHandler.createComputeResource(computeResourceDTO); - LOGGER.info("Created compute resource with ID: {}", savedResource.getComputeResourceId()); + LOGGER.info("Created compute resource with ID: {} by user: {}", savedResource.getComputeResourceId(), currentUser); return ResponseEntity.status(HttpStatus.CREATED).body(savedResource); } catch (Exception e) { @@ -131,6 +139,7 @@ public ResponseEntity createComputeResource(@Valid @RequestBody ComputeResour @Operation(summary = "Update compute resource") @PutMapping("/{id}") + @PreAuthorize("hasRole('USER') or hasRole('API_USER')") public ResponseEntity updateComputeResource(@PathVariable("id") String id, @Valid @RequestBody ComputeResourceDTO computeResourceDTO, BindingResult bindingResult) { LOGGER.info("Updating compute resource with ID: {}", id); @@ -144,6 +153,9 @@ public ResponseEntity updateComputeResource(@PathVariable("id") String id, @V return ResponseEntity.badRequest().body("Validation failed: " + errorMessage); } + // Set intelligent defaults for fields not provided by UI + setDefaultValues(computeResourceDTO); + try { ComputeResourceDTO updatedResource = computeResourceHandler.updateComputeResource(id, computeResourceDTO); LOGGER.info("Successfully updated compute resource with ID: {}", id); @@ -158,6 +170,7 @@ public ResponseEntity updateComputeResource(@PathVariable("id") String id, @V @Operation(summary = "Delete compute resource") @DeleteMapping("/{id}") + @PreAuthorize("hasRole('USER') or hasRole('API_USER')") public ResponseEntity deleteComputeResource(@PathVariable("id") String id) { LOGGER.info("Deleting compute resource with ID: {}", id); @@ -191,14 +204,16 @@ public ResponseEntity> searchComputeResources( @Operation(summary = "Star/unstar a compute resource") @PostMapping("/{id}/star") + @PreAuthorize("hasRole('USER')") public ResponseEntity starComputeResource(@PathVariable("id") String id) { LOGGER.info("Toggling star for compute resource with ID: {}", id); try { + String userId = userContextService.getCurrentUserId(); if (computeResourceHandler.existsComputeResource(id)) { // TODO: Implement proper v1 ResourceStar system integration // For now, return simple toggle response - LOGGER.info("Star toggle requested for compute resource: {} (simplified implementation)", id); + LOGGER.info("Star toggle requested for compute resource: {} by user: {} (simplified implementation)", id, userId); return ResponseEntity.ok(true); } else { LOGGER.warn("Compute resource not found with ID: {}", id); @@ -265,4 +280,73 @@ public ResponseEntity> getStarredComputeResources() { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } + + /** + * Set intelligent defaults for backend-only fields not provided by UI + * UI provides core fields (hostName, name, description) - backend fills in infrastructure defaults + */ + private void setDefaultValues(ComputeResourceDTO dto) { + // Backend fills infrastructure defaults - UI provides name and resourceDescription + + // Set default compute type + if (dto.getComputeType() == null || dto.getComputeType().trim().isEmpty()) { + dto.setComputeType("HPC"); + } + + // Set default CPU cores + if (dto.getCpuCores() == null) { + dto.setCpuCores(1); + } + + // Set default memory + if (dto.getMemoryGB() == null) { + dto.setMemoryGB(1); + } + + // Set default operating system + if (dto.getOperatingSystem() == null || dto.getOperatingSystem().trim().isEmpty()) { + dto.setOperatingSystem("Linux"); + } + + // Set default queue system + if (dto.getQueueSystem() == null || dto.getQueueSystem().trim().isEmpty()) { + dto.setQueueSystem("SLURM"); + } + + // Set default resource manager + if (dto.getResourceManager() == null || dto.getResourceManager().trim().isEmpty()) { + dto.setResourceManager("Default Resource Manager"); + } + + // Set default SSH configuration + if (dto.getSshUsername() == null || dto.getSshUsername().trim().isEmpty()) { + dto.setSshUsername("admin"); + } + + if (dto.getSshPort() == null) { + dto.setSshPort(22); + } + + if (dto.getAuthenticationMethod() == null || dto.getAuthenticationMethod().trim().isEmpty()) { + dto.setAuthenticationMethod("SSH_KEYS"); + } + + // Set default working directory + if (dto.getWorkingDirectory() == null || dto.getWorkingDirectory().trim().isEmpty()) { + dto.setWorkingDirectory("/tmp"); + } + + // Set default scheduler type + if (dto.getSchedulerType() == null || dto.getSchedulerType().trim().isEmpty()) { + dto.setSchedulerType("SLURM"); + } + + // Set default data movement protocol + if (dto.getDataMovementProtocol() == null || dto.getDataMovementProtocol().trim().isEmpty()) { + dto.setDataMovementProtocol("SCP"); + } + + LOGGER.debug("Set default values for compute resource: name={}, type={}, cores={}", + dto.getName(), dto.getComputeType(), dto.getCpuCores()); + } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java index 96973a5ca8c..3babdb1c18c 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java @@ -25,9 +25,11 @@ import java.util.List; import org.apache.airavata.research.service.dto.StorageResourceDTO; import org.apache.airavata.research.service.handler.StorageResourceHandler; +import org.apache.airavata.research.service.service.UserContextService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindingResult; @@ -51,8 +53,12 @@ public class StorageResourceController { @Autowired private StorageResourceHandler storageResourceHandler; - @Operation(summary = "Get all public storage resources") - @GetMapping("/public") + @Autowired + private UserContextService userContextService; + + @Operation(summary = "Get all storage resources") + @GetMapping("/") + @PreAuthorize("hasRole('USER') or hasRole('API_USER')") public ResponseEntity> getStorageResources( @RequestParam(value = "nameSearch", required = false) String nameSearch) { @@ -76,7 +82,8 @@ public ResponseEntity> getStorageResources( } @Operation(summary = "Get storage resource by ID") - @GetMapping("/public/{id}") + @GetMapping("/{id}") + @PreAuthorize("hasRole('USER') or hasRole('API_USER')") public ResponseEntity getStorageResourceById(@PathVariable("id") String id) { LOGGER.info("Getting storage resource by ID: {}", id); @@ -95,6 +102,7 @@ public ResponseEntity getStorageResourceById(@PathVariable(" @Operation(summary = "Create new storage resource") @PostMapping("/") + @PreAuthorize("hasRole('USER') or hasRole('API_USER')") public ResponseEntity createStorageResource(@Valid @RequestBody StorageResourceDTO storageResourceDTO, BindingResult bindingResult) { LOGGER.info("Creating new storage resource: {}", storageResourceDTO.getHostName()); @@ -108,11 +116,10 @@ public ResponseEntity createStorageResource(@Valid @RequestBody StorageResour return ResponseEntity.badRequest().body("Validation failed: " + errorMessage); } + // Set intelligent defaults for fields not provided by UI + setDefaultValues(storageResourceDTO); + try { - // Set default values for fields that might be null - if (storageResourceDTO.getCapacityTB() == null) { - storageResourceDTO.setCapacityTB(1L); // Default to 1 TB - } StorageResourceDTO savedResource = storageResourceHandler.createStorageResource(storageResourceDTO); LOGGER.info("Created storage resource with ID: {}", savedResource.getStorageResourceId()); @@ -140,6 +147,9 @@ public ResponseEntity updateStorageResource(@PathVariable("id") String id, @V return ResponseEntity.badRequest().body("Validation failed: " + errorMessage); } + // Set intelligent defaults for fields not provided by UI + setDefaultValues(storageResourceDTO); + try { StorageResourceDTO updatedResource = storageResourceHandler.updateStorageResource(id, storageResourceDTO); LOGGER.info("Successfully updated storage resource with ID: {}", id); @@ -278,4 +288,40 @@ public ResponseEntity> getStarredStorageResources() { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } + + /** + * Set intelligent defaults for backend-only fields not provided by UI + * UI provides core fields (hostName, name, description) - backend fills in infrastructure defaults + */ + private void setDefaultValues(StorageResourceDTO dto) { + // Backend fills infrastructure defaults - UI provides name + + // Set default storage type + if (dto.getStorageType() == null || dto.getStorageType().trim().isEmpty()) { + dto.setStorageType("SCP"); + } + + // Set default capacity + if (dto.getCapacityTB() == null) { + dto.setCapacityTB(1L); + } + + // Set default access protocol based on storage type + if (dto.getAccessProtocol() == null || dto.getAccessProtocol().trim().isEmpty()) { + if ("S3".equalsIgnoreCase(dto.getStorageType())) { + dto.setAccessProtocol("HTTPS"); + } else { + dto.setAccessProtocol("SCP"); + } + } + + // Set default endpoint based on hostname + if (dto.getEndpoint() == null || dto.getEndpoint().trim().isEmpty()) { + String hostname = dto.getHostName(); + dto.setEndpoint(hostname != null ? hostname : "localhost"); + } + + LOGGER.debug("Set default values for storage resource: name={}, type={}, capacity={}TB", + dto.getName(), dto.getStorageType(), dto.getCapacityTB()); + } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/resources/application.yml b/modules/research-framework/research-service/src/main/resources/application.yml index 7a4c1798c18..8b1dde7bf28 100644 --- a/modules/research-framework/research-service/src/main/resources/application.yml +++ b/modules/research-framework/research-service/src/main/resources/application.yml @@ -89,3 +89,19 @@ springdoc: use-pkce-with-authorization-code-grant: true client-id: data-catalog-portal +# Authentication Configuration +research: + auth: + jwks-uri: "https://auth.dev.cybershuttle.org/.well-known/jwks" + dev-api-key: "dev-research-api-key-12345" + cors: + allowed-origins: "http://localhost:5173,http://localhost:3000" + allowed-methods: "GET,POST,PUT,DELETE,OPTIONS" + allowed-headers: "*" + +# Logging for security debugging (dev profile) +logging: + level: + org.springframework.security: DEBUG + org.springframework.security.oauth2: DEBUG + From 85146d9834743adafc1ccabc1a4531d24efb4fcd Mon Sep 17 00:00:00 2001 From: Krish Katariya Date: Sat, 2 Aug 2025 21:49:50 -0700 Subject: [PATCH 13/17] Fixing compute resources bugs --- .../service/entity/ComputeResourceEntity.java | 2 + .../service/entity/StorageResourceEntity.java | 2 + .../research/service/util/DTOConverter.java | 90 +++- .../service/v2/config/V2DataInitializer.java | 329 --------------- .../service/v2/controller/CodeController.java | 397 ------------------ .../research/service/v2/entity/Code.java | 215 ---------- .../service/v2/repository/CodeRepository.java | 109 ----- 7 files changed, 72 insertions(+), 1072 deletions(-) delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/CodeRepository.java diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/ComputeResourceEntity.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/ComputeResourceEntity.java index 403c0a738eb..9ba4a035889 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/ComputeResourceEntity.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/ComputeResourceEntity.java @@ -15,6 +15,7 @@ public class ComputeResourceEntity implements Serializable { @Column(name = "HOST_NAME", nullable = false) private String hostName; + @Column(name = "RESOURCE_DESCRIPTION", length = 2048) private String resourceDescription; @@ -67,6 +68,7 @@ public String getHostName() { public void setHostName(String hostName) { this.hostName = hostName; } + public String getResourceDescription() { return resourceDescription; diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/StorageResourceEntity.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/StorageResourceEntity.java index 22c191af7d4..eefa4c6c0ee 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/StorageResourceEntity.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/StorageResourceEntity.java @@ -15,6 +15,7 @@ public class StorageResourceEntity implements Serializable { @Column(name = "HOST_NAME", nullable = false) private String hostName; + @Column(name = "DESCRIPTION", length = 2048) private String description; @@ -43,6 +44,7 @@ public String getHostName() { public void setHostName(String hostName) { this.hostName = hostName; } + public String getDescription() { return description; diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java index 421bd1f8b74..5bb442efd76 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java @@ -21,20 +21,17 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.*; +import java.util.stream.Collectors; import org.apache.airavata.model.appcatalog.computeresource.BatchQueue; import org.apache.airavata.model.appcatalog.computeresource.ComputeResourceDescription; import org.apache.airavata.model.appcatalog.storageresource.StorageResourceDescription; -import org.apache.airavata.research.service.entity.ComputeResourceEntity; -import org.apache.airavata.research.service.entity.StorageResourceEntity; import org.apache.airavata.research.service.dto.ComputeResourceDTO; import org.apache.airavata.research.service.dto.ComputeResourceQueueDTO; import org.apache.airavata.research.service.dto.StorageResourceDTO; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; +import org.apache.airavata.research.service.entity.ComputeResourceEntity; +import org.apache.airavata.research.service.entity.StorageResourceEntity; -import java.util.*; -import java.util.stream.Collectors; /** * Utility class for converting between entities and DTOs @@ -496,9 +493,10 @@ public StorageResourceDTO storageEntityToDTO(StorageResourceEntity entity) { // Extract name from UI fields or generate fallback String extractedName = extractNameFromStorageDescription(entity.getDescription()); - if (extractedName != null) { + if (extractedName != null && !extractedName.trim().isEmpty()) { dto.setName(extractedName); } else { + // Generate name from hostname and description dto.setName(generateStorageResourceName(entity.getHostName(), entity.getDescription())); } @@ -560,9 +558,10 @@ public ComputeResourceDTO computeEntityToDTO(ComputeResourceEntity entity) { // Extract name from UI fields or generate fallback String extractedName = extractNameFromDescription(entity.getResourceDescription()); - if (extractedName != null) { + if (extractedName != null && !extractedName.trim().isEmpty()) { dto.setName(extractedName); } else { + // Generate name from hostname and description dto.setName(generateComputeResourceName(entity.getHostName(), entity.getResourceDescription())); } @@ -675,6 +674,15 @@ private void extractComputeUIFieldsFromDescription(String description, ComputeRe dto.setOperatingSystem(getStringValue(rootNode, OPERATING_SYSTEM_KEY)); dto.setSchedulerType(getStringValue(rootNode, SCHEDULER_TYPE_KEY)); dto.setDataMovementProtocol(getStringValue(rootNode, DATA_MOVEMENT_PROTOCOL_KEY)); + dto.setQueueSystem(getStringValue(rootNode, QUEUE_SYSTEM_KEY)); + dto.setResourceManager(getStringValue(rootNode, RESOURCE_MANAGER_KEY)); + dto.setWorkingDirectory(getStringValue(rootNode, WORKING_DIR_KEY)); + + // Extract SSH fields + dto.setSshUsername(getStringValue(rootNode, SSH_USERNAME_KEY)); + dto.setSshPort(getIntegerValue(rootNode, SSH_PORT_KEY)); + dto.setAuthenticationMethod(getStringValue(rootNode, AUTH_METHOD_KEY)); + dto.setSshKey(getStringValue(rootNode, SSH_KEY_KEY)); // Extract preserved fields dto.setName(getStringValue(rootNode, NAME_KEY)); @@ -798,6 +806,15 @@ private String encodeComputeUIFieldsIntoDescription(ComputeResourceDTO dto) { uiFields.put(OPERATING_SYSTEM_KEY, dto.getOperatingSystem()); uiFields.put(SCHEDULER_TYPE_KEY, dto.getSchedulerType()); uiFields.put(DATA_MOVEMENT_PROTOCOL_KEY, dto.getDataMovementProtocol()); + uiFields.put(QUEUE_SYSTEM_KEY, dto.getQueueSystem()); + uiFields.put(RESOURCE_MANAGER_KEY, dto.getResourceManager()); + uiFields.put(WORKING_DIR_KEY, dto.getWorkingDirectory()); + + // SSH configuration fields + uiFields.put(SSH_USERNAME_KEY, dto.getSshUsername()); + uiFields.put(SSH_PORT_KEY, dto.getSshPort()); + uiFields.put(AUTH_METHOD_KEY, dto.getAuthenticationMethod()); + uiFields.put(SSH_KEY_KEY, dto.getSshKey()); // Preserve critical fields that might be lost uiFields.put(NAME_KEY, dto.getName()); @@ -828,20 +845,35 @@ private String encodeComputeUIFieldsIntoDescription(ComputeResourceDTO dto) { private String generateStorageResourceName(String hostName, String description) { if (description != null && description.length() > 10) { // Try to extract first line/sentence as name - String firstLine = description.split("\n")[0]; + String firstLine = description.split("\n")[0].trim(); if (firstLine.length() > 5 && firstLine.length() < 100) { return firstLine; } } // Fallback to hostname-based name - if (hostName != null) { - return hostName.replace(".edu", "") - .replace("-", " ") - .replace(".", " "); + if (hostName != null && !hostName.trim().isEmpty()) { + String name = hostName.replace(".edu", "") + .replace(".org", "") + .replace(".com", "") + .replace("-", " ") + .replace(".", " "); + + // Capitalize words for better display + String[] words = name.split("\\s+"); + StringBuilder result = new StringBuilder(); + for (String word : words) { + if (word.length() > 0) { + if (result.length() > 0) result.append(" "); + result.append(word.substring(0, 1).toUpperCase()) + .append(word.substring(1).toLowerCase()); + } + } + return result.toString(); } - return "Storage Resource"; + // Ultimate fallback if hostname is also null/empty + return "Unnamed Storage Resource"; } /** @@ -850,21 +882,35 @@ private String generateStorageResourceName(String hostName, String description) private String generateComputeResourceName(String hostName, String description) { if (description != null && description.length() > 10) { // Try to extract first line/sentence as name - String firstLine = description.split("\n")[0]; + String firstLine = description.split("\n")[0].trim(); if (firstLine.length() > 5 && firstLine.length() < 100) { return firstLine; } } // Fallback to hostname-based name - if (hostName != null) { - return hostName.replace(".edu", "") - .replace(".org", "") - .replace("-", " ") - .replace(".", " "); + if (hostName != null && !hostName.trim().isEmpty()) { + String name = hostName.replace(".edu", "") + .replace(".org", "") + .replace(".com", "") + .replace("-", " ") + .replace(".", " "); + + // Capitalize words for better display + String[] words = name.split("\\s+"); + StringBuilder result = new StringBuilder(); + for (String word : words) { + if (word.length() > 0) { + if (result.length() > 0) result.append(" "); + result.append(word.substring(0, 1).toUpperCase()) + .append(word.substring(1).toLowerCase()); + } + } + return result.toString(); } - return "Compute Resource"; + // Ultimate fallback if hostname is also null/empty + return "Unnamed Compute Resource"; } /** diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java deleted file mode 100644 index e2c8b8d326d..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/config/V2DataInitializer.java +++ /dev/null @@ -1,329 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.config; - -import jakarta.annotation.PostConstruct; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import org.apache.airavata.research.service.enums.PrivacyEnum; -import org.apache.airavata.research.service.enums.StateEnum; -import org.apache.airavata.research.service.enums.StatusEnum; -import org.apache.airavata.research.service.model.entity.Tag; -import org.apache.airavata.research.service.model.repo.TagRepository; -import org.apache.airavata.research.service.v2.entity.Code; -import org.apache.airavata.research.service.v2.repository.CodeRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -/** - * V2 Data Initializer for Code entities - * Storage resources now use airavata-api registry services (following migration.md) - */ -@Component -public class V2DataInitializer { - - private static final Logger LOGGER = LoggerFactory.getLogger(V2DataInitializer.class); - - private final CodeRepository codeRepository; - private final TagRepository tagRepository; - - public V2DataInitializer(CodeRepository codeRepository, - TagRepository tagRepository) { - this.codeRepository = codeRepository; - this.tagRepository = tagRepository; - } - - @PostConstruct - public void initializeData() { - LOGGER.info("Initializing V2 mock data for code resources..."); - - try { - initializeCodes(); - - LOGGER.info("V2 mock data initialization completed."); - } catch (Exception e) { - LOGGER.error("Error during V2 data initialization: {}", e.getMessage(), e); - throw new RuntimeException("Failed to initialize V2 mock data", e); - } - } - - private void initializeCodes() { - if (codeRepository.count() == 0) { - LOGGER.info("Creating mock code resources..."); - - // Research-focused sample data based on v1 patterns and real deployments - CodeData[] codeDataArray = { - // Neuroscience Research Models (inspired by v1 neurodata25 projects) - new CodeData( - "Bio-realistic Cortical Circuit Simulation Model", - "Running the AllenAI V1 model for bio-realistic multiscale simulations of cortical circuits with thalamacortical and background inputs", - "MODEL", - "Python", - "NEURON", - Set.of("anton.arkhipov@alleninstitute.org", "laura.green@alleninstitute.org"), - Set.of("neurodata25", "allenai", "visual-cortex", "neuroscience", "simulation"), - "allenai_v1_model.pkl", - "1.0", - null, - null, - "Bio-realistic cortical circuit model with thalamacortical inputs. Part of NeuroData25 initiative." - ), - - new CodeData( - "Apache Cerebrum Computational Model", - "Constructing computational neuroscience models from large public databases and brain atlases using Apache Airavata middleware", - "MODEL", - "Python", - "Apache Airavata", - Set.of("sriram.chockalingam@apache.org"), - Set.of("neurodata25", "apache", "cerebrum", "brain-atlases", "computational-neuroscience"), - "cerebrum_brain_model_v2.h5", - "2.0", - null, - null, - "Large-scale brain modeling framework using public neuroscience databases." - ), - - new CodeData( - "Biologically Constrained RNN Model", - "Biologically constrained recurrent neural network via Dale's backpropagation and topologically-informed pruning for neural computation", - "MODEL", - "Python", - "PyTorch", - Set.of("hannah.choi@gatech.edu", "aishwarya.balwani@gatech.edu"), - Set.of("neurodata25", "hchoilab", "biological-rnn", "dale-principle", "neural-networks"), - "biological_rnn_trained.pth", - "1.2", - null, - null, - "Biologically plausible RNN with Dale's law constraints and topological pruning." - ), - - // Computational Chemistry Models (inspired by SQL dump applications) - new CodeData( - "PSI4 Quantum Chemistry Model", - "OPENMP Psi4 application for ab initio quantum chemistry programs designed for efficient, high-accuracy simulations of molecular properties", - "MODEL", - "Python", - "PSI4", - Set.of("quantum.team@psi4.org", "ccguser@chemistry.org"), - Set.of("quantum-chemistry", "ab-initio", "molecular-simulation", "psi4", "computational-chemistry"), - "psi4_optimized_model.wfn", - "1.8", - null, - null, - "High-accuracy quantum chemistry calculations with OPENMP parallelization." - ), - - new CodeData( - "AlphaFold2 Protein Structure Model", - "Protein structure prediction using locally deployed AlphaFold2 singularity container for accurate protein folding prediction", - "MODEL", - "Python", - "JAX", - Set.of("deepmind.team@google.com", "scigap@alphafold.org"), - Set.of("protein-folding", "alphafold", "structural-biology", "deep-learning", "bioinformatics"), - "alphafold2_weights.pkl", - "2.3", - null, - null, - "State-of-the-art protein structure prediction using AlphaFold2 architecture." - ), - - // Research Notebooks (based on real scientific workflows) - new CodeData( - "Whole-Brain Sleep Dynamics Analysis", - "Jupyter notebook for analyzing spatio-temporal dynamics of sleep in large-scale brain models during awake and sleep states", - "NOTEBOOK", - "Python", - "Jupyter", - Set.of("maxim.bazhenov@ucsd.edu", "gabriela.navas@ucsd.edu"), - Set.of("neurodata25", "bazhlab", "whole-brain", "sleep-dynamics", "neuroscience"), - "sleep_dynamics_analysis.ipynb", - "3.1", - null, - null, - "Comprehensive analysis of sleep-related brain activity patterns using large-scale modeling." - ), - - new CodeData( - "One-hot HMM-GLM Brain State Discovery", - "Implementation of One-hot Generalized Linear Model for switching brain state discovery, reproducing ICLR 2024 paper findings", - "NOTEBOOK", - "Python", - "JAX", - Set.of("anqi.wu@nyu.edu", "chengrui.li@nyu.edu"), - Set.of("neurodata25", "brainml", "hmm-glm", "brain-states", "machine-learning"), - "onehot_hmmglm_analysis.ipynb", - "1.0", - null, - null, - "Advanced statistical modeling for brain state identification using GLM framework." - ), - - new CodeData( - "NAMD Molecular Dynamics Workflow", - "Comprehensive molecular dynamics simulation workflow using NAMD with GPU acceleration for protein-ligand interactions", - "NOTEBOOK", - "Tcl", - "NAMD", - Set.of("md.researcher@illinois.edu", "namd.support@ks.uiuc.edu"), - Set.of("molecular-dynamics", "namd", "protein-simulation", "gpu-computing", "hpc"), - "namd_md_workflow.ipynb", - "2.14", - null, - null, - "Production molecular dynamics workflows with NAMD 2.14 and GPU support." - ), - - // Research Code Repositories (following v1 GitHub pattern) - new CodeData( - "Neural Oscillators Computing Framework", - "A comprehensive framework for computing with neural oscillators, including speech processing demos and neuromorphic computing applications", - "REPOSITORY", - "Python", - "NumPy", - Set.of("nabil.imam@intel.com", "nand.chandravadia@intel.com"), - Set.of("neurodata25", "imamlab", "neural-oscillators", "neuromorphic", "speech-processing"), - null, - null, - "https://github.com/cyber-shuttle/imamlab-neural-oscillators", - "main", - "Neural oscillator-based computing for neuromorphic applications and speech processing." - ), - - new CodeData( - "Torch Brain and TemporalData Toolkit", - "Scaling up neural data analysis with torch_brain and temporaldata libraries for large-scale neuroscience data processing", - "REPOSITORY", - "Python", - "PyTorch", - Set.of("eva.dyer@gatech.edu", "vinam.arora@gatech.edu", "mahato.shivashriganesh@gatech.edu"), - Set.of("neurodata25", "nerdslab", "torch-brain", "temporaldata", "neuroscience"), - null, - null, - "https://github.com/cyber-shuttle/neurodata25_torchbrain_notebooks", - "main", - "Advanced neural data analysis tools with PyTorch integration for large-scale processing." - ), - - new CodeData( - "NetFormer Neural Connectivity Model", - "Running the NetFormer model to bridge the gap between structure and function in the brain using transformer architectures", - "REPOSITORY", - "Python", - "Transformers", - Set.of("lu.mi@neuroaihub.org"), - Set.of("neurodata25", "neuroaihub", "netformer", "neural-connectivity", "transformers"), - null, - null, - "https://github.com/cyber-shuttle/neuroaihub-netformer", - "main", - "Transformer-based analysis of neural connectivity patterns in brain networks." - ) - }; - - // Create codes from sample data - for (CodeData codeData : codeDataArray) { - Code code = createCodeFromData(codeData); - codeRepository.save(code); - } - - LOGGER.info("Created {} code resources", codeDataArray.length); - } - } - - private Code createCodeFromData(CodeData data) { - Code code = new Code(); - code.setName(data.name); - code.setDescription(data.description); - code.setCodeType(data.codeType); - code.setProgrammingLanguage(data.programmingLanguage); - code.setFramework(data.framework); - code.setVersion(data.version); - code.setFileName(data.fileName); - code.setRepositoryUrl(data.repositoryUrl); - code.setBranch(data.branch); - code.setAdditionalInfo(data.additionalInfo); - - // Set default v1 Resource fields (inherited) - code.setPrivacy(PrivacyEnum.PUBLIC); - code.setState(StateEnum.ACTIVE); - code.setStatus(StatusEnum.VERIFIED); - code.setAuthors(new HashSet<>(data.authors)); - code.setTags(getOrCreateTags(data.tags)); - code.setHeaderImage(""); // Default empty header image - - return code; - } - - private Set getOrCreateTags(Set tagNames) { - Set tags = new HashSet<>(); - for (String tagName : tagNames) { - Tag existingTag = tagRepository.findByValue(tagName); - if (existingTag != null) { - tags.add(existingTag); - } else { - Tag newTag = new Tag(); - newTag.setValue(tagName); - Tag savedTag = tagRepository.save(newTag); - tags.add(savedTag); - } - } - return tags; - } - - - // Helper class for organizing sample data - private static class CodeData { - final String name; - final String description; - final String codeType; // MODEL, NOTEBOOK, REPOSITORY - final String programmingLanguage; - final String framework; - final Set authors; - final Set tags; - final String fileName; // For models and notebooks - final String version; - final String repositoryUrl; // For repositories - final String branch; // For repositories - final String additionalInfo; - - public CodeData(String name, String description, String codeType, String programmingLanguage, - String framework, Set authors, Set tags, String fileName, - String version, String repositoryUrl, String branch, String additionalInfo) { - this.name = name; - this.description = description; - this.codeType = codeType; - this.programmingLanguage = programmingLanguage; - this.framework = framework; - this.authors = authors; - this.tags = tags; - this.fileName = fileName; - this.version = version; - this.repositoryUrl = repositoryUrl; - this.branch = branch; - this.additionalInfo = additionalInfo; - } - } -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java deleted file mode 100644 index b34146afa88..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/CodeController.java +++ /dev/null @@ -1,397 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import java.util.List; -import java.util.Optional; -import org.apache.airavata.research.service.enums.PrivacyEnum; -import org.apache.airavata.research.service.enums.StateEnum; -import org.apache.airavata.research.service.v2.entity.Code; -import org.apache.airavata.research.service.v2.repository.CodeRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.security.access.prepost.PreAuthorize; - -@RestController -@RequestMapping("/api/v2/rf/codes") -@Tag(name = "Code Resources V2", description = "V2 API for managing code resources (models, notebooks, repositories)") -public class CodeController { - - private static final Logger LOGGER = LoggerFactory.getLogger(CodeController.class); - private static final PrivacyEnum PUBLIC_PRIVACY = PrivacyEnum.PUBLIC; - private static final StateEnum ACTIVE_STATE = StateEnum.ACTIVE; - - private final CodeRepository codeRepository; - - public CodeController(CodeRepository codeRepository) { - this.codeRepository = codeRepository; - } - - @Operation(summary = "Get all codes with pagination") - @GetMapping("/") - @PreAuthorize("hasRole('USER') or hasRole('API_USER')") - public ResponseEntity> getCodes( - @RequestParam(value = "pageNumber", defaultValue = "0") int pageNumber, - @RequestParam(value = "pageSize", defaultValue = "10") int pageSize, - @RequestParam(value = "keyword", required = false) String keyword, - @RequestParam(value = "codeType", required = false) String codeType, - @RequestParam(value = "programmingLanguage", required = false) String programmingLanguage) { - - LOGGER.info("Getting codes - page: {}, size: {}, keyword: {}, type: {}, language: {}", - pageNumber, pageSize, keyword, codeType, programmingLanguage); - - Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending()); - Page codes; - - if (keyword != null && !keyword.trim().isEmpty()) { - codes = codeRepository.findByKeywordSearchAndPrivacyAndState(keyword, PUBLIC_PRIVACY, ACTIVE_STATE, pageable); - } else { - codes = codeRepository.findByPrivacyAndState(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); - } - - LOGGER.info("Found {} codes", codes.getTotalElements()); - return ResponseEntity.ok(codes); - } - - @Operation(summary = "Get code by ID") - @GetMapping("/{id}") - @PreAuthorize("hasRole('USER') or hasRole('API_USER')") - public ResponseEntity getCodeById(@PathVariable("id") String id) { - LOGGER.info("Getting code by ID: {}", id); - - Optional code = codeRepository.findById(id); - if (code.isPresent()) { - return ResponseEntity.ok(code.get()); - } else { - LOGGER.warn("Code not found with ID: {}", id); - return ResponseEntity.notFound().build(); - } - } - - @Operation(summary = "Create new code") - @PostMapping("/") - public ResponseEntity createCode(@Valid @RequestBody Code code, BindingResult bindingResult) { - LOGGER.info("Creating new code: {}", code.getName()); - - // Validation error handling - if (bindingResult.hasErrors()) { - String errorMessage = bindingResult.getFieldErrors().stream() - .map(error -> error.getField() + ": " + error.getDefaultMessage()) - .reduce((msg1, msg2) -> msg1 + ", " + msg2) - .orElse("Validation failed"); - LOGGER.error("Validation errors: {}", errorMessage); - return ResponseEntity.badRequest().body("Validation failed: " + errorMessage); - } - - try { - // Set default values using enums - if (code.getPrivacy() == null) { - code.setPrivacy(PUBLIC_PRIVACY); - } - if (code.getState() == null) { - code.setState(ACTIVE_STATE); - } - // Note: starCount functionality handled separately in v1 star system - - Code savedCode = codeRepository.save(code); - LOGGER.info("Created code with ID: {}", savedCode.getId()); - - return ResponseEntity.status(HttpStatus.CREATED).body(savedCode); - } catch (Exception e) { - LOGGER.error("Error creating code: {}", e.getMessage(), e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Error creating code: " + e.getMessage()); - } - } - - @Operation(summary = "Update code") - @PutMapping("/{id}") - public ResponseEntity updateCode(@PathVariable("id") String id, @Valid @RequestBody Code code, BindingResult bindingResult) { - LOGGER.info("Updating code with ID: {}", id); - - // Validation error handling - if (bindingResult.hasErrors()) { - String errorMessage = bindingResult.getFieldErrors().stream() - .map(error -> error.getField() + ": " + error.getDefaultMessage()) - .reduce((msg1, msg2) -> msg1 + ", " + msg2) - .orElse("Validation failed"); - LOGGER.error("Validation errors: {}", errorMessage); - return ResponseEntity.badRequest().body("Validation failed: " + errorMessage); - } - - try { - Optional existingCode = codeRepository.findById(id); - if (!existingCode.isPresent()) { - LOGGER.warn("Code not found with ID: {}", id); - return ResponseEntity.notFound().build(); - } - - // Set the ID to ensure we update the correct code - code.setId(id); - - // Preserve creation timestamp - code.setCreatedAt(existingCode.get().getCreatedAt()); - - Code updatedCode = codeRepository.save(code); - LOGGER.info("Successfully updated code with ID: {}", id); - - return ResponseEntity.ok(updatedCode); - } catch (Exception e) { - LOGGER.error("Error updating code with ID: {}", id, e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Error updating code: " + e.getMessage()); - } - } - - @Operation(summary = "Delete code") - @DeleteMapping("/{id}") - public ResponseEntity deleteCode(@PathVariable("id") String id) { - LOGGER.info("Deleting code with ID: {}", id); - - try { - Optional existingCode = codeRepository.findById(id); - if (!existingCode.isPresent()) { - LOGGER.warn("Code not found with ID: {}", id); - return ResponseEntity.notFound().build(); - } - - codeRepository.deleteById(id); - LOGGER.info("Successfully deleted code with ID: {}", id); - return ResponseEntity.ok().body("Code deleted successfully"); - } catch (Exception e) { - LOGGER.error("Error deleting code with ID: {}", id, e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Error deleting code: " + e.getMessage()); - } - } - - @Operation(summary = "Search codes by keyword") - @GetMapping("/search") - public ResponseEntity> searchCodes( - @RequestParam(value = "keyword") String keyword) { - - LOGGER.info("Searching codes with keyword: {}", keyword); - - List codes = codeRepository.findByKeywordSearchAndPrivacyAndState(keyword, PUBLIC_PRIVACY, ACTIVE_STATE); - - LOGGER.info("Found {} codes matching keyword: {}", codes.size(), keyword); - return ResponseEntity.ok(codes); - } - - @Operation(summary = "Get codes by type") - @GetMapping("/type/{codeType}") - public ResponseEntity> getCodesByType( - @PathVariable("codeType") String codeType) { - - LOGGER.info("Getting codes by type: {}", codeType); - - List codes = codeRepository.findByCodeTypeAndPrivacyAndState(codeType, PUBLIC_PRIVACY, ACTIVE_STATE); - - LOGGER.info("Found {} codes of type: {}", codes.size(), codeType); - return ResponseEntity.ok(codes); - } - - @Operation(summary = "Get codes by programming language") - @GetMapping("/language/{programmingLanguage}") - public ResponseEntity> getCodesByLanguage( - @PathVariable("programmingLanguage") String programmingLanguage) { - - LOGGER.info("Getting codes by programming language: {}", programmingLanguage); - - List codes = codeRepository.findByProgrammingLanguageAndPrivacyAndState(programmingLanguage, PUBLIC_PRIVACY, ACTIVE_STATE); - - LOGGER.info("Found {} codes for language: {}", codes.size(), programmingLanguage); - return ResponseEntity.ok(codes); - } - - @Operation(summary = "Get codes by framework") - @GetMapping("/framework/{framework}") - public ResponseEntity> getCodesByFramework( - @PathVariable("framework") String framework) { - - LOGGER.info("Getting codes by framework: {}", framework); - - List codes = codeRepository.findByFrameworkAndPrivacyAndState(framework, PUBLIC_PRIVACY, ACTIVE_STATE); - - LOGGER.info("Found {} codes for framework: {}", codes.size(), framework); - return ResponseEntity.ok(codes); - } - - @Operation(summary = "Get codes by tag") - @GetMapping("/tag/{tag}") - public ResponseEntity> getCodesByTag( - @PathVariable("tag") String tag) { - - LOGGER.info("Getting codes by tag: {}", tag); - - List codes = codeRepository.findByTagAndPrivacyAndState(tag, PUBLIC_PRIVACY, ACTIVE_STATE); - - LOGGER.info("Found {} codes with tag: {}", codes.size(), tag); - return ResponseEntity.ok(codes); - } - - @Operation(summary = "Get codes by author") - @GetMapping("/author/{author}") - public ResponseEntity> getCodesByAuthor( - @PathVariable("author") String author) { - - LOGGER.info("Getting codes by author: {}", author); - - List codes = codeRepository.findByAuthorAndPrivacyAndState(author, PUBLIC_PRIVACY, ACTIVE_STATE); - - LOGGER.info("Found {} codes by author: {}", codes.size(), author); - return ResponseEntity.ok(codes); - } - - @Operation(summary = "Get top starred codes") - @GetMapping("/top-starred") - public ResponseEntity> getTopStarredCodes( - @RequestParam(value = "limit", defaultValue = "10") int limit) { - - LOGGER.info("Getting top {} starred codes", limit); - - Pageable pageable = PageRequest.of(0, limit); - List codes = codeRepository.findTopStarredCodes(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); - - LOGGER.info("Found {} top starred codes", codes.size()); - return ResponseEntity.ok(codes); - } - - @Operation(summary = "Get recent codes") - @GetMapping("/recent") - public ResponseEntity> getRecentCodes( - @RequestParam(value = "limit", defaultValue = "10") int limit) { - - LOGGER.info("Getting {} recent codes", limit); - - Pageable pageable = PageRequest.of(0, limit); - List codes = codeRepository.findRecentCodes(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); - - LOGGER.info("Found {} recent codes", codes.size()); - return ResponseEntity.ok(codes); - } - - @Operation(summary = "Star/unstar a code") - @PostMapping("/{id}/star") - public ResponseEntity starCode(@PathVariable("id") String id) { - LOGGER.info("Toggling star for code with ID: {}", id); - - try { - Optional codeOpt = codeRepository.findById(id); - if (codeOpt.isPresent()) { - Code code = codeOpt.get(); - - // TODO: Implement proper v1 ResourceStar system integration - // For now, return simple toggle response - LOGGER.info("Star toggle requested for code: {} (simplified implementation)", id); - return ResponseEntity.ok(true); - } else { - LOGGER.warn("Code not found with ID: {}", id); - return ResponseEntity.notFound().build(); - } - } catch (Exception e) { - LOGGER.error("Error toggling code star: {}", e.getMessage(), e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } - - @Operation(summary = "Check if user starred a code") - @GetMapping("/{id}/star") - public ResponseEntity checkCodeStarred(@PathVariable("id") String id) { - LOGGER.info("Checking if code is starred: {}", id); - - try { - Optional codeOpt = codeRepository.findById(id); - if (codeOpt.isPresent()) { - Code code = codeOpt.get(); - // TODO: Implement proper v1 ResourceStar system integration - LOGGER.info("Star status check for code: {} (simplified implementation)", id); - return ResponseEntity.ok(false); - } else { - LOGGER.warn("Code not found with ID: {}", id); - return ResponseEntity.notFound().build(); - } - } catch (Exception e) { - LOGGER.error("Error checking code star status: {}", e.getMessage(), e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } - - @Operation(summary = "Get code star count") - @GetMapping("/{id}/stars/count") - public ResponseEntity getCodeStarCount(@PathVariable("id") String id) { - LOGGER.info("Getting star count for code: {}", id); - - try { - Optional codeOpt = codeRepository.findById(id); - if (codeOpt.isPresent()) { - // TODO: Implement proper v1 ResourceStar system integration - return ResponseEntity.ok(0); - } else { - LOGGER.warn("Code not found with ID: {}", id); - return ResponseEntity.notFound().build(); - } - } catch (Exception e) { - LOGGER.error("Error getting star count: {}", e.getMessage(), e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } - - @Operation(summary = "Get all starred codes") - @GetMapping("/starred") - public ResponseEntity> getStarredCodes( - @RequestParam(value = "page", defaultValue = "0") int page, - @RequestParam(value = "size", defaultValue = "50") int size) { - LOGGER.info("Fetching starred codes - page: {}, size: {}", page, size); - - try { - Pageable pageable = PageRequest.of(page, size); - // TODO: Implement proper v1 ResourceStar system integration - // For now, return empty page - Page starredCodes = codeRepository.findByPrivacyAndState(PUBLIC_PRIVACY, ACTIVE_STATE, pageable); - // Filter to empty for now until proper star system is implemented - starredCodes = Page.empty(); - LOGGER.info("Found {} starred codes", starredCodes.getTotalElements()); - return ResponseEntity.ok(starredCodes); - } catch (Exception e) { - LOGGER.error("Error fetching starred codes: {}", e.getMessage(), e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java deleted file mode 100644 index aa62b425cb7..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/entity/Code.java +++ /dev/null @@ -1,215 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.entity; - -import jakarta.persistence.CollectionTable; -import jakarta.persistence.Column; -import jakarta.persistence.ElementCollection; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.Table; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; -import java.util.HashSet; -import java.util.Set; -import org.apache.airavata.research.service.enums.PrivacyEnum; -import org.apache.airavata.research.service.enums.ResourceTypeEnum; -import org.apache.airavata.research.service.enums.StateEnum; -import org.apache.airavata.research.service.enums.StatusEnum; -import org.apache.airavata.research.service.model.entity.Resource; -import org.apache.airavata.research.service.model.entity.Tag; - -@Entity -@Table(name = "CODE_V2") -public class Code extends Resource { - - @Column(nullable = false) - @NotBlank(message = "Code type is required") - @Size(max = 100, message = "Code type must not exceed 100 characters") - private String codeType; // MODEL, NOTEBOOK, REPOSITORY, HYBRID - - // From ModelResource - @Column - private String applicationInterfaceId; - - @Column - @Size(max = 50, message = "Version must not exceed 50 characters") - private String version; - - // From NotebookResource - @Column - private String notebookPath; - - // From RepositoryResource - @Column - private String repositoryUrl; - - // Combined metadata fields - @Column - @Size(max = 100, message = "Programming language must not exceed 100 characters") - private String programmingLanguage; // Python, R, Java, etc. - - @Column - @Size(max = 100, message = "Framework must not exceed 100 characters") - private String framework; // TensorFlow, PyTorch, Scikit-learn, etc. - - @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "code_dependencies", joinColumns = @JoinColumn(name = "code_id")) - @Column(name = "dependency") - private Set dependencies = new HashSet<>(); - - @Column(columnDefinition = "TEXT") - private String additionalInfo; - - @Override - public ResourceTypeEnum getType() { - return ResourceTypeEnum.CODE; - } - - // Default constructor - public Code() {} - - // Main constructor for creating code entities - public Code(String name, String description, String codeType, String programmingLanguage, - String framework, Set authors, Set tags) { - this.setName(name); - this.setDescription(description); - this.codeType = codeType; - this.programmingLanguage = programmingLanguage; - this.framework = framework; - // Set inherited v1 Resource fields (required) - this.setPrivacy(PrivacyEnum.PUBLIC); - this.setState(StateEnum.ACTIVE); - this.setStatus(StatusEnum.VERIFIED); - this.setAuthors(authors != null ? authors : new HashSet<>()); - this.setTags(tags != null ? tags : new HashSet<>()); - this.setHeaderImage(""); // Default empty header image - } - - // Getters and Setters for Code-specific fields - public String getCodeType() { - return codeType; - } - - public void setCodeType(String codeType) { - this.codeType = codeType; - } - - public String getApplicationInterfaceId() { - return applicationInterfaceId; - } - - public void setApplicationInterfaceId(String applicationInterfaceId) { - this.applicationInterfaceId = applicationInterfaceId; - } - - public String getVersion() { - return version; - } - - public void setVersion(String version) { - this.version = version; - } - - public String getNotebookPath() { - return notebookPath; - } - - public void setNotebookPath(String notebookPath) { - this.notebookPath = notebookPath; - } - - public String getRepositoryUrl() { - return repositoryUrl; - } - - public void setRepositoryUrl(String repositoryUrl) { - this.repositoryUrl = repositoryUrl; - } - - public String getProgrammingLanguage() { - return programmingLanguage; - } - - public void setProgrammingLanguage(String programmingLanguage) { - this.programmingLanguage = programmingLanguage; - } - - public String getFramework() { - return framework; - } - - public void setFramework(String framework) { - this.framework = framework; - } - - public Set getDependencies() { - return dependencies; - } - - public void setDependencies(Set dependencies) { - this.dependencies = dependencies; - } - - public String getAdditionalInfo() { - return additionalInfo; - } - - public void setAdditionalInfo(String additionalInfo) { - this.additionalInfo = additionalInfo; - } - - // Additional setter methods for V2DataInitializer compatibility - public void setFileName(String fileName) { - // For models and notebooks, store in notebookPath field - this.notebookPath = fileName; - } - - public String getFileName() { - return this.notebookPath; - } - - public void setBranch(String branch) { - // For repositories, use additionalInfo to store branch info - if (branch != null && !branch.isEmpty()) { - String branchInfo = "Branch: " + branch; - if (this.additionalInfo != null && !this.additionalInfo.isEmpty()) { - this.additionalInfo += "; " + branchInfo; - } else { - this.additionalInfo = branchInfo; - } - } - } - - public String getBranch() { - // Extract branch info from additionalInfo - if (additionalInfo != null && additionalInfo.contains("Branch: ")) { - String[] parts = additionalInfo.split("Branch: "); - if (parts.length > 1) { - String branchPart = parts[1]; - // Get everything before the next semicolon - int semicolonIndex = branchPart.indexOf(";"); - return semicolonIndex > 0 ? branchPart.substring(0, semicolonIndex) : branchPart; - } - } - return null; - } -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/CodeRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/CodeRepository.java deleted file mode 100644 index b10ddfe7c10..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/repository/CodeRepository.java +++ /dev/null @@ -1,109 +0,0 @@ -/** -* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -*/ -package org.apache.airavata.research.service.v2.repository; - -import java.util.List; -import org.apache.airavata.research.service.enums.PrivacyEnum; -import org.apache.airavata.research.service.enums.StateEnum; -import org.apache.airavata.research.service.v2.entity.Code; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -@Repository -public interface CodeRepository extends JpaRepository { - - // Find by name containing (case insensitive) - List findByNameContainingIgnoreCaseAndPrivacyAndState(String name, PrivacyEnum privacy, StateEnum state); - - // Find by code type - List findByCodeTypeAndPrivacyAndState(String codeType, PrivacyEnum privacy, StateEnum state); - - // Find by programming language - List findByProgrammingLanguageAndPrivacyAndState(String programmingLanguage, PrivacyEnum privacy, StateEnum state); - - // Find by framework - List findByFrameworkAndPrivacyAndState(String framework, PrivacyEnum privacy, StateEnum state); - - // Find all public and active codes with pagination - Page findByPrivacyAndState(PrivacyEnum privacy, StateEnum state, Pageable pageable); - - // Search by name, description, or tags with pagination - @Query("SELECT c FROM Code c WHERE c.privacy = :privacy AND c.state = :state AND " + - "(LOWER(c.name) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + - "LOWER(c.description) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + - "LOWER(c.codeType) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + - "LOWER(c.programmingLanguage) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + - "LOWER(c.framework) LIKE LOWER(CONCAT('%', :keyword, '%')))") - Page findByKeywordSearchAndPrivacyAndState(@Param("keyword") String keyword, - @Param("privacy") PrivacyEnum privacy, - @Param("state") StateEnum state, - Pageable pageable); - - // Search codes by keyword (for simple list) - @Query("SELECT c FROM Code c WHERE c.privacy = :privacy AND c.state = :state AND " + - "(LOWER(c.name) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + - "LOWER(c.description) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + - "LOWER(c.codeType) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + - "LOWER(c.programmingLanguage) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " + - "LOWER(c.framework) LIKE LOWER(CONCAT('%', :keyword, '%')))") - List findByKeywordSearchAndPrivacyAndState(@Param("keyword") String keyword, - @Param("privacy") PrivacyEnum privacy, - @Param("state") StateEnum state); - - // Find all public and active codes - List findAllByPrivacyAndState(PrivacyEnum privacy, StateEnum state); - - // Find codes by tag - @Query("SELECT c FROM Code c JOIN c.tags t WHERE c.privacy = :privacy AND c.state = :state AND LOWER(t.value) = LOWER(:tag)") - List findByTagAndPrivacyAndState(@Param("tag") String tag, - @Param("privacy") PrivacyEnum privacy, - @Param("state") StateEnum state); - - // Find codes by author - @Query("SELECT c FROM Code c JOIN c.authors a WHERE c.privacy = :privacy AND c.state = :state AND LOWER(a) LIKE LOWER(CONCAT('%', :author, '%'))") - List findByAuthorAndPrivacyAndState(@Param("author") String author, - @Param("privacy") PrivacyEnum privacy, - @Param("state") StateEnum state); - - // Find codes by dependency - @Query("SELECT c FROM Code c JOIN c.dependencies d WHERE c.privacy = :privacy AND c.state = :state AND LOWER(d) = LOWER(:dependency)") - List findByDependencyAndPrivacyAndState(@Param("dependency") String dependency, - @Param("privacy") PrivacyEnum privacy, - @Param("state") StateEnum state); - - // Find top starred codes (TODO: implement proper v1 ResourceStar integration) - @Query("SELECT c FROM Code c WHERE c.privacy = :privacy AND c.state = :state ORDER BY c.createdAt DESC") - List findTopStarredCodes(@Param("privacy") PrivacyEnum privacy, - @Param("state") StateEnum state, - Pageable pageable); - - // Find recently created codes - @Query("SELECT c FROM Code c WHERE c.privacy = :privacy AND c.state = :state ORDER BY c.createdAt DESC") - List findRecentCodes(@Param("privacy") PrivacyEnum privacy, - @Param("state") StateEnum state, - Pageable pageable); - - // TODO: Remove this method - implement proper v1 ResourceStar integration - // Temporarily removed starCount-based method -} \ No newline at end of file From 4b9e0ebc97cf83142aea78b8735228330cdc4a5d Mon Sep 17 00:00:00 2001 From: Krish Katariya Date: Fri, 8 Aug 2025 16:27:43 -0700 Subject: [PATCH 14/17] v2 API w/airavata-api imports and entity mapping fixes --- .../appcatalog/AppEnvironmentEntity.java | 2 +- .../appcatalog/AppModuleMappingEntity.java | 4 +- .../appcatalog/ApplicationInputEntity.java | 2 +- .../appcatalog/ApplicationOutputEntity.java | 2 +- .../entities/appcatalog/BatchQueueEntity.java | 2 +- .../BatchQueueResourcePolicyEntity.java | 2 +- .../appcatalog/ComputeResourceEntity.java | 38 ++-- .../ComputeResourceFileSystemEntity.java | 2 +- .../ComputeResourcePolicyEntity.java | 2 +- .../ComputeResourcePreferenceEntity.java | 2 +- .../DataMovementInterfaceEntity.java | 2 +- .../appcatalog/GridftpEndpointEntity.java | 2 +- .../GroupComputeResourcePrefEntity.java | 2 +- .../GroupSSHAccountProvisionerConfig.java | 4 +- .../appcatalog/JobManagerCommandEntity.java | 2 +- .../JobSubmissionInterfaceEntity.java | 2 +- .../appcatalog/LibraryApendPathEntity.java | 2 +- .../appcatalog/LibraryPrependPathEntity.java | 2 +- .../appcatalog/LocalSubmissionEntity.java | 2 +- .../appcatalog/ModuleLoadCmdEntity.java | 2 +- .../appcatalog/ParallelismCommandEntity.java | 2 +- .../appcatalog/ParserConnectorEntity.java | 6 +- .../ParserConnectorInputEntity.java | 6 +- .../appcatalog/ParserInputEntity.java | 2 +- .../appcatalog/ParserOutputEntity.java | 2 +- .../ParsingTemplateInputEntity.java | 4 +- .../appcatalog/PostjobCommandEntity.java | 2 +- .../appcatalog/PrejobCommandEntity.java | 2 +- .../SSHAccountProvisionerConfiguration.java | 4 +- .../appcatalog/SshJobSubmissionEntity.java | 11 ++ .../appcatalog/StorageInterfaceEntity.java | 2 +- .../appcatalog/StoragePreferenceEntity.java | 2 +- .../appcatalog/StorageResourceEntity.java | 12 +- .../UserComputeResourcePreferenceEntity.java | 4 +- .../UserStoragePreferenceEntity.java | 4 +- .../config/AppCatalogDatabaseConfig.java | 4 +- .../service/dto/ComputeResourceDTO.java | 82 +++------ .../service/entity/ComputeResourceEntity.java | 168 ------------------ .../service/entity/StorageResourceEntity.java | 80 --------- .../handler/ComputeResourceHandler.java | 8 +- .../handler/StorageResourceHandler.java | 4 +- .../repository/ComputeResourceRepository.java | 2 +- .../repository/StorageResourceRepository.java | 6 +- .../research/service/util/DTOConverter.java | 141 ++++++++++----- .../controller/ComputeResourceController.java | 27 ++- .../controller/StorageResourceController.java | 4 +- 46 files changed, 230 insertions(+), 441 deletions(-) delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/ComputeResourceEntity.java delete mode 100644 modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/StorageResourceEntity.java diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/AppEnvironmentEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/AppEnvironmentEntity.java index 14fa6f3f6d0..c0bb5bda7cf 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/AppEnvironmentEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/AppEnvironmentEntity.java @@ -34,7 +34,7 @@ public class AppEnvironmentEntity implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "DEPLOYMENT_ID") + @Column(name = "DEPLOYMENT_ID", insertable = false, updatable = false) private String deploymentId; @Column(name = "VALUE") diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/AppModuleMappingEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/AppModuleMappingEntity.java index ede77fe1105..aaf2d7ddf1b 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/AppModuleMappingEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/AppModuleMappingEntity.java @@ -33,11 +33,11 @@ public class AppModuleMappingEntity implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "INTERFACE_ID") + @Column(name = "INTERFACE_ID", insertable = false, updatable = false) private String interfaceId; @Id - @Column(name = "MODULE_ID") + @Column(name = "MODULE_ID", insertable = false, updatable = false) private String moduleId; @ManyToOne(targetEntity = ApplicationInterfaceEntity.class) diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ApplicationInputEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ApplicationInputEntity.java index c471c8dfcd7..b45dcdc7355 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ApplicationInputEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ApplicationInputEntity.java @@ -36,7 +36,7 @@ public class ApplicationInputEntity implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "INTERFACE_ID") + @Column(name = "INTERFACE_ID", insertable = false, updatable = false) private String interfaceId; @Id diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ApplicationOutputEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ApplicationOutputEntity.java index c47011f44a0..682141d4c12 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ApplicationOutputEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ApplicationOutputEntity.java @@ -35,7 +35,7 @@ public class ApplicationOutputEntity implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "INTERFACE_ID") + @Column(name = "INTERFACE_ID", insertable = false, updatable = false) private String interfaceId; @Id diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/BatchQueueEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/BatchQueueEntity.java index a7222621b70..ec892b0e9fb 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/BatchQueueEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/BatchQueueEntity.java @@ -32,7 +32,7 @@ public class BatchQueueEntity implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "COMPUTE_RESOURCE_ID") + @Column(name = "COMPUTE_RESOURCE_ID", insertable = false, updatable = false) private String computeResourceId; @Id diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/BatchQueueResourcePolicyEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/BatchQueueResourcePolicyEntity.java index fec67cd7d77..ab59b20e757 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/BatchQueueResourcePolicyEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/BatchQueueResourcePolicyEntity.java @@ -45,7 +45,7 @@ public class BatchQueueResourcePolicyEntity implements Serializable { @Column(name = "COMPUTE_RESOURCE_ID") private String computeResourceId; - @Column(name = "GROUP_RESOURCE_PROFILE_ID") + @Column(name = "GROUP_RESOURCE_PROFILE_ID", insertable = false, updatable = false) private String groupResourceProfileId; @Column(name = "QUEUE_NAME") diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ComputeResourceEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ComputeResourceEntity.java index d77db7d8737..cb856d74e2a 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ComputeResourceEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ComputeResourceEntity.java @@ -33,10 +33,11 @@ public class ComputeResourceEntity implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "RESOURCE_ID") + @Column(name = "RESOURCE_ID", nullable = false, length = 255) private String computeResourceId; - @Column(name = "CREATION_TIME") + @Column(name = "CREATION_TIME", nullable = false) + @Temporal(TemporalType.TIMESTAMP) private Timestamp creationTime; @Column(name = "ENABLED") @@ -49,9 +50,9 @@ public class ComputeResourceEntity implements Serializable { private String gatewayUsageModuleLoadCommand; @Column(name = "GATEWAY_USAGE_REPORTING") - private boolean gatewayUsageReporting; + private Boolean gatewayUsageReporting; - @Column(name = "HOST_NAME") + @Column(name = "HOST_NAME", nullable = false, length = 255) private String hostName; @Column(name = "MAX_MEMORY_NODE") @@ -60,7 +61,8 @@ public class ComputeResourceEntity implements Serializable { @Column(name = "RESOURCE_DESCRIPTION") private String resourceDescription; - @Column(name = "UPDATE_TIME") + @Column(name = "UPDATE_TIME", nullable = false) + @Temporal(TemporalType.TIMESTAMP) private Timestamp updateTime; @Column(name = "CPUS_PER_NODE") @@ -75,13 +77,21 @@ public class ComputeResourceEntity implements Serializable { @Column(name = "DEFAULT_WALLTIME") private Integer defaultWalltime; - @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "HOST_ALIAS", joinColumns = @JoinColumn(name = "RESOURCE_ID")) + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable( + name = "HOST_ALIAS", + joinColumns = @JoinColumn(name = "RESOURCE_ID"), + foreignKey = @ForeignKey(name = "host_alias_ibfk_1") + ) @Column(name = "ALIAS") private List hostAliases; - @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "HOST_IPADDRESS", joinColumns = @JoinColumn(name = "RESOURCE_ID")) + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable( + name = "HOST_IPADDRESS", + joinColumns = @JoinColumn(name = "RESOURCE_ID"), + foreignKey = @ForeignKey(name = "host_ipaddress_ibfk_1") + ) @Column(name = "IP_ADDRESS") private List ipAddresses; @@ -89,21 +99,21 @@ public class ComputeResourceEntity implements Serializable { targetEntity = BatchQueueEntity.class, cascade = CascadeType.ALL, mappedBy = "computeResource", - fetch = FetchType.EAGER) + fetch = FetchType.LAZY) private List batchQueues; @OneToMany( targetEntity = JobSubmissionInterfaceEntity.class, cascade = CascadeType.ALL, mappedBy = "computeResource", - fetch = FetchType.EAGER) + fetch = FetchType.LAZY) private List jobSubmissionInterfaces; @OneToMany( targetEntity = DataMovementInterfaceEntity.class, cascade = CascadeType.ALL, mappedBy = "computeResource", - fetch = FetchType.EAGER) + fetch = FetchType.LAZY) private List dataMovementInterfaces; public ComputeResourceEntity() {} @@ -132,11 +142,11 @@ public void setGatewayUsageExecutable(String gatewayUsageExecutable) { this.gatewayUsageExecutable = gatewayUsageExecutable; } - public boolean isGatewayUsageReporting() { + public Boolean isGatewayUsageReporting() { return gatewayUsageReporting; } - public void setGatewayUsageReporting(boolean gatewayUsageReporting) { + public void setGatewayUsageReporting(Boolean gatewayUsageReporting) { this.gatewayUsageReporting = gatewayUsageReporting; } diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ComputeResourceFileSystemEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ComputeResourceFileSystemEntity.java index 84ff4dfe6a0..659134fa0f4 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ComputeResourceFileSystemEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ComputeResourceFileSystemEntity.java @@ -33,7 +33,7 @@ public class ComputeResourceFileSystemEntity implements Serializable { private static final long serialVersionUID = 1L; - @Column(name = "COMPUTE_RESOURCE_ID") + @Column(name = "COMPUTE_RESOURCE_ID", insertable = false, updatable = false) @Id private String computeResourceId; diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ComputeResourcePolicyEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ComputeResourcePolicyEntity.java index 85b41062e9a..629c8ad04a0 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ComputeResourcePolicyEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ComputeResourcePolicyEntity.java @@ -49,7 +49,7 @@ public class ComputeResourcePolicyEntity implements Serializable { @Column(name = "COMPUTE_RESOURCE_ID") private String computeResourceId; - @Column(name = "GROUP_RESOURCE_PROFILE_ID") + @Column(name = "GROUP_RESOURCE_PROFILE_ID", insertable = false, updatable = false) private String groupResourceProfileId; // TODO: Store COMPUTE_RESOURCE_ID and QUEUE_NAME in table so it can FK to BATCH_QUEUE diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ComputeResourcePreferenceEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ComputeResourcePreferenceEntity.java index 0a316359d85..d7c48b296e5 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ComputeResourcePreferenceEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ComputeResourcePreferenceEntity.java @@ -35,7 +35,7 @@ public class ComputeResourcePreferenceEntity implements Serializable { private static final long serialVersionUID = 1L; - @Column(name = "GATEWAY_ID") + @Column(name = "GATEWAY_ID", insertable = false, updatable = false) @Id private String gatewayId; diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/DataMovementInterfaceEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/DataMovementInterfaceEntity.java index 21e1f2a0382..5473a24f9f7 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/DataMovementInterfaceEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/DataMovementInterfaceEntity.java @@ -33,7 +33,7 @@ public class DataMovementInterfaceEntity implements Serializable { private static final long serialVersionUID = 1L; - @Column(name = "COMPUTE_RESOURCE_ID") + @Column(name = "COMPUTE_RESOURCE_ID", insertable = false, updatable = false) @Id private String computeResourceId; diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/GridftpEndpointEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/GridftpEndpointEntity.java index add631d2066..e1b8b198c43 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/GridftpEndpointEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/GridftpEndpointEntity.java @@ -33,7 +33,7 @@ public class GridftpEndpointEntity implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "DATA_MOVEMENT_INTERFACE_ID") + @Column(name = "DATA_MOVEMENT_INTERFACE_ID", insertable = false, updatable = false) private String dataMovementInterfaceId; @Id diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/GroupComputeResourcePrefEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/GroupComputeResourcePrefEntity.java index 7c0cd30d5fe..82b0ae34ef2 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/GroupComputeResourcePrefEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/GroupComputeResourcePrefEntity.java @@ -55,7 +55,7 @@ public abstract class GroupComputeResourcePrefEntity implements Serializable { @Id private String computeResourceId; - @Column(name = "GROUP_RESOURCE_PROFILE_ID") + @Column(name = "GROUP_RESOURCE_PROFILE_ID", insertable = false, updatable = false) @Id private String groupResourceProfileId; diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/GroupSSHAccountProvisionerConfig.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/GroupSSHAccountProvisionerConfig.java index b117df577e6..1e1b9d1ba3d 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/GroupSSHAccountProvisionerConfig.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/GroupSSHAccountProvisionerConfig.java @@ -42,11 +42,11 @@ public class GroupSSHAccountProvisionerConfig implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "RESOURCE_ID") + @Column(name = "RESOURCE_ID", insertable = false, updatable = false) private String resourceId; @Id - @Column(name = "GROUP_RESOURCE_PROFILE_ID") + @Column(name = "GROUP_RESOURCE_PROFILE_ID", insertable = false, updatable = false) private String groupResourceProfileId; @Id diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/JobManagerCommandEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/JobManagerCommandEntity.java index d4c5256d916..9113dbe6783 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/JobManagerCommandEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/JobManagerCommandEntity.java @@ -33,7 +33,7 @@ public class JobManagerCommandEntity implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "RESOURCE_JOB_MANAGER_ID") + @Column(name = "RESOURCE_JOB_MANAGER_ID", insertable = false, updatable = false) private String resourceJobManagerId; @Id diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/JobSubmissionInterfaceEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/JobSubmissionInterfaceEntity.java index 6bea6a63c71..40500d68c78 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/JobSubmissionInterfaceEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/JobSubmissionInterfaceEntity.java @@ -33,7 +33,7 @@ public class JobSubmissionInterfaceEntity implements Serializable { private static final long serialVersionUID = 1L; - @Column(name = "COMPUTE_RESOURCE_ID") + @Column(name = "COMPUTE_RESOURCE_ID", insertable = false, updatable = false) @Id private String computeResourceId; diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/LibraryApendPathEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/LibraryApendPathEntity.java index 8afe6efbf36..262bc469291 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/LibraryApendPathEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/LibraryApendPathEntity.java @@ -34,7 +34,7 @@ public class LibraryApendPathEntity implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "DEPLOYMENT_ID") + @Column(name = "DEPLOYMENT_ID", insertable = false, updatable = false) private String deploymentId; @Column(name = "VALUE") diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/LibraryPrependPathEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/LibraryPrependPathEntity.java index 7c61c0d0725..d573279ddaa 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/LibraryPrependPathEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/LibraryPrependPathEntity.java @@ -35,7 +35,7 @@ public class LibraryPrependPathEntity implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "DEPLOYMENT_ID") + @Column(name = "DEPLOYMENT_ID", insertable = false, updatable = false) private String deploymentId; @Column(name = "VALUE") diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/LocalSubmissionEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/LocalSubmissionEntity.java index 9b7b62a20ce..d7df4bf3d98 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/LocalSubmissionEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/LocalSubmissionEntity.java @@ -42,7 +42,7 @@ public class LocalSubmissionEntity implements Serializable { @Column(name = "UPDATE_TIME") private Timestamp updateTime; - @Column(name = "RESOURCE_JOB_MANAGER_ID") + @Column(name = "RESOURCE_JOB_MANAGER_ID", insertable = false, updatable = false) private String resourceJobManagerId; @Column(name = "SECURITY_PROTOCOL") diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ModuleLoadCmdEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ModuleLoadCmdEntity.java index fae4d7087e9..05c700cf0af 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ModuleLoadCmdEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ModuleLoadCmdEntity.java @@ -34,7 +34,7 @@ public class ModuleLoadCmdEntity implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "APP_DEPLOYMENT_ID") + @Column(name = "APP_DEPLOYMENT_ID", insertable = false, updatable = false) private String appdeploymentId; @Id diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParallelismCommandEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParallelismCommandEntity.java index be4b15d7cff..97afa83921d 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParallelismCommandEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParallelismCommandEntity.java @@ -33,7 +33,7 @@ public class ParallelismCommandEntity implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "RESOURCE_JOB_MANAGER_ID") + @Column(name = "RESOURCE_JOB_MANAGER_ID", insertable = false, updatable = false) private String resourceJobManagerId; @Id diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParserConnectorEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParserConnectorEntity.java index 8147e238ba4..7d691c8c2c4 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParserConnectorEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParserConnectorEntity.java @@ -32,13 +32,13 @@ public class ParserConnectorEntity implements Serializable { @Column(name = "PARSER_CONNECTOR_ID") private String id; - @Column(name = "PARENT_PARSER_ID") + @Column(name = "PARENT_PARSER_ID", insertable = false, updatable = false) private String parentParserId; - @Column(name = "CHILD_PARSER_ID") + @Column(name = "CHILD_PARSER_ID", insertable = false, updatable = false) private String childParserId; - @Column(name = "PARSING_TEMPLATE_ID") + @Column(name = "PARSING_TEMPLATE_ID", insertable = false, updatable = false) private String parsingTemplateId; @OneToMany( diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParserConnectorInputEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParserConnectorInputEntity.java index 2a758cdb4ef..243ca7ad0a6 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParserConnectorInputEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParserConnectorInputEntity.java @@ -31,16 +31,16 @@ public class ParserConnectorInputEntity implements Serializable { @Column(name = "PARSER_CONNECTOR_INPUT_ID") private String id; - @Column(name = "PARSER_INPUT_ID") + @Column(name = "PARSER_INPUT_ID", insertable = false, updatable = false) private String inputId; - @Column(name = "PARSER_OUTPUT_ID") + @Column(name = "PARSER_OUTPUT_ID", insertable = false, updatable = false) private String parentOutputId; @Column(name = "VALUE") private String value; - @Column(name = "PARSER_CONNECTOR_ID") + @Column(name = "PARSER_CONNECTOR_ID", insertable = false, updatable = false) private String parserConnectorId; @ManyToOne(targetEntity = ParserInputEntity.class, cascade = CascadeType.MERGE) diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParserInputEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParserInputEntity.java index e37e4c2405b..ba1dbf9a279 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParserInputEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParserInputEntity.java @@ -37,7 +37,7 @@ public class ParserInputEntity implements Serializable { @Column(name = "PARSER_INPUT_REQUIRED") private boolean requiredInput; - @Column(name = "PARSER_ID") + @Column(name = "PARSER_ID", insertable = false, updatable = false) private String parserId; @ManyToOne(targetEntity = ParserEntity.class, cascade = CascadeType.MERGE) diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParserOutputEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParserOutputEntity.java index 63c2fc0a05a..b1a790afc21 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParserOutputEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParserOutputEntity.java @@ -37,7 +37,7 @@ public class ParserOutputEntity implements Serializable { @Column(name = "PARSER_OUTPUT_REQUIRED") private boolean requiredOutput; - @Column(name = "PARSER_ID") + @Column(name = "PARSER_ID", insertable = false, updatable = false) private String parserId; @ManyToOne(targetEntity = ParserEntity.class, cascade = CascadeType.MERGE) diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParsingTemplateInputEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParsingTemplateInputEntity.java index d538299fa77..d2766b5b1b0 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParsingTemplateInputEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/ParsingTemplateInputEntity.java @@ -31,7 +31,7 @@ public class ParsingTemplateInputEntity implements Serializable { @Column(name = "PARSING_TEMPLATE_INPUT_ID") private String id; - @Column(name = "TARGET_PARSER_INPUT_ID") + @Column(name = "TARGET_PARSER_INPUT_ID", insertable = false, updatable = false) private String targetInputId; @Column(name = "APPLICATION_OUTPUT_NAME") @@ -40,7 +40,7 @@ public class ParsingTemplateInputEntity implements Serializable { @Column(name = "VALUE") private String value; - @Column(name = "PARSING_TEMPLATE_ID") + @Column(name = "PARSING_TEMPLATE_ID", insertable = false, updatable = false) private String parsingTemplateId; @ManyToOne(targetEntity = ParserInputEntity.class, cascade = CascadeType.MERGE) diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/PostjobCommandEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/PostjobCommandEntity.java index bf330dfcc85..1930cbfbe70 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/PostjobCommandEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/PostjobCommandEntity.java @@ -34,7 +34,7 @@ public class PostjobCommandEntity implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "APPDEPLOYMENT_ID") + @Column(name = "APPDEPLOYMENT_ID", insertable = false, updatable = false) private String appdeploymentId; @Id diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/PrejobCommandEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/PrejobCommandEntity.java index 81b0b510764..f99895d1e68 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/PrejobCommandEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/PrejobCommandEntity.java @@ -34,7 +34,7 @@ public class PrejobCommandEntity implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "APPDEPLOYMENT_ID") + @Column(name = "APPDEPLOYMENT_ID", insertable = false, updatable = false) private String appdeploymentId; @Id diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/SSHAccountProvisionerConfiguration.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/SSHAccountProvisionerConfiguration.java index 420d83570bc..c1c6a171d08 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/SSHAccountProvisionerConfiguration.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/SSHAccountProvisionerConfiguration.java @@ -30,11 +30,11 @@ @IdClass(SSHAccountProvisionerConfigurationPK.class) public class SSHAccountProvisionerConfiguration { @Id - @Column(name = "GATEWAY_ID") + @Column(name = "GATEWAY_ID", insertable = false, updatable = false) private String gatewayId; @Id - @Column(name = "RESOURCE_ID") + @Column(name = "RESOURCE_ID", insertable = false, updatable = false) private String resourceId; @Id diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/SshJobSubmissionEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/SshJobSubmissionEntity.java index de83512076a..1f956e508e7 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/SshJobSubmissionEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/SshJobSubmissionEntity.java @@ -38,6 +38,9 @@ public class SshJobSubmissionEntity implements Serializable { @Column(name = "JOB_SUBMISSION_INTERFACE_ID") private String jobSubmissionInterfaceId; + @Column(name = "RESOURCE_JOB_MANAGER_ID", insertable = false, updatable = false) + private String resourceJobManagerId; + @ManyToOne(cascade = CascadeType.MERGE) @JoinColumn(name = "RESOURCE_JOB_MANAGER_ID", nullable = false, updatable = false) private ResourceJobManagerEntity resourceJobManager; @@ -71,6 +74,14 @@ public void setJobSubmissionInterfaceId(String jobSubmissionInterfaceId) { this.jobSubmissionInterfaceId = jobSubmissionInterfaceId; } + public String getResourceJobManagerId() { + return resourceJobManagerId; + } + + public void setResourceJobManagerId(String resourceJobManagerId) { + this.resourceJobManagerId = resourceJobManagerId; + } + public String getAlternativeSshHostname() { return alternativeSshHostname; } diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/StorageInterfaceEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/StorageInterfaceEntity.java index 60cff0bbaaf..8ca150ccd3e 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/StorageInterfaceEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/StorageInterfaceEntity.java @@ -34,7 +34,7 @@ public class StorageInterfaceEntity implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "STORAGE_RESOURCE_ID") + @Column(name = "STORAGE_RESOURCE_ID", insertable = false, updatable = false) private String storageResourceId; @Id diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/StoragePreferenceEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/StoragePreferenceEntity.java index e0a53b33df2..3f3ed6c2dec 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/StoragePreferenceEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/StoragePreferenceEntity.java @@ -31,7 +31,7 @@ public class StoragePreferenceEntity implements Serializable { private static final long serialVersionUID = 1L; - @Column(name = "GATEWAY_ID") + @Column(name = "GATEWAY_ID", insertable = false, updatable = false) @Id private String gatewayId; diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/StorageResourceEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/StorageResourceEntity.java index c17d489dace..9e5aa03c733 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/StorageResourceEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/StorageResourceEntity.java @@ -33,10 +33,11 @@ public class StorageResourceEntity implements Serializable { private static final long serialVersionUID = 1L; @Id - @Column(name = "STORAGE_RESOURCE_ID") + @Column(name = "STORAGE_RESOURCE_ID", nullable = false, length = 255) private String storageResourceId; - @Column(name = "CREATION_TIME") + @Column(name = "CREATION_TIME", nullable = false) + @Temporal(TemporalType.TIMESTAMP) private Timestamp creationTime; @Column(name = "DESCRIPTION") @@ -45,17 +46,18 @@ public class StorageResourceEntity implements Serializable { @Column(name = "ENABLED") private boolean enabled; - @Column(name = "HOST_NAME") + @Column(name = "HOST_NAME", nullable = false, length = 255) private String hostName; - @Column(name = "UPDATE_TIME") + @Column(name = "UPDATE_TIME", nullable = false) + @Temporal(TemporalType.TIMESTAMP) private Timestamp updateTime; @OneToMany( targetEntity = StorageInterfaceEntity.class, cascade = CascadeType.ALL, mappedBy = "storageResource", - fetch = FetchType.EAGER) + fetch = FetchType.LAZY) private List dataMovementInterfaces; public StorageResourceEntity() {} diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/UserComputeResourcePreferenceEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/UserComputeResourcePreferenceEntity.java index 246a8394ea9..f66da07b5d2 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/UserComputeResourcePreferenceEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/UserComputeResourcePreferenceEntity.java @@ -36,11 +36,11 @@ public class UserComputeResourcePreferenceEntity { private String computeResourceId; @Id - @Column(name = "USER_ID") + @Column(name = "USER_ID", insertable = false, updatable = false) private String userId; @Id - @Column(name = "GATEWAY_ID") + @Column(name = "GATEWAY_ID", insertable = false, updatable = false) private String gatewayId; @Column(name = "PREFERED_BATCH_QUEUE") diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/UserStoragePreferenceEntity.java b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/UserStoragePreferenceEntity.java index e3ae98ca6d5..08700f395c0 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/UserStoragePreferenceEntity.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/core/entities/appcatalog/UserStoragePreferenceEntity.java @@ -35,11 +35,11 @@ public class UserStoragePreferenceEntity { private String storageResourceId; @Id - @Column(name = "USER_ID") + @Column(name = "USER_ID", insertable = false, updatable = false) private String userId; @Id - @Column(name = "GATEWAY_ID") + @Column(name = "GATEWAY_ID", insertable = false, updatable = false) private String gatewayId; @Column(name = "RESOURCE_CS_TOKEN") diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AppCatalogDatabaseConfig.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AppCatalogDatabaseConfig.java index fb9cfcbb1bf..c4637c4fe27 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AppCatalogDatabaseConfig.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AppCatalogDatabaseConfig.java @@ -58,8 +58,8 @@ public LocalContainerEntityManagerFactoryBean appCatalogEntityManagerFactory() { em.setDataSource(appCatalogDataSource()); em.setPersistenceUnitName("appCatalogPU"); - // Scan our local entities that mirror database schema exactly - em.setPackagesToScan("org.apache.airavata.research.service.entity"); + // Scan airavata-api entities instead of local replicated entities + em.setPackagesToScan("org.apache.airavata.registry.core.entities.appcatalog"); HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); vendorAdapter.setGenerateDdl(false); // Don't modify existing schema diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/ComputeResourceDTO.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/ComputeResourceDTO.java index e5c09eba61f..a8098bc840a 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/ComputeResourceDTO.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/ComputeResourceDTO.java @@ -65,46 +65,30 @@ public class ComputeResourceDTO { @Size(max = 100, message = "Operating system must not exceed 100 characters") private String operatingSystem; - @NotBlank(message = "Queue system is required") - @Size(max = 100, message = "Queue system must not exceed 100 characters") + // Queue system is represented by BatchQueue entities, not a simple string + // Keeping for backward compatibility but making optional private String queueSystem; // SLURM, PBS, SGE, etc. private String additionalInfo; - @NotBlank(message = "Resource manager is required") - @Size(max = 255, message = "Resource manager must not exceed 255 characters") + // Resource manager type handled by ResourceJobManager entity + // Keeping for backward compatibility but making optional private String resourceManager; // Gateway name or organization // Direct mappings from ComputeResourceDescription private List hostAliases = new ArrayList<>(); private List ipAddresses = new ArrayList<>(); - // UI-specific SSH configuration fields - @NotBlank(message = "SSH username is required") - @Size(max = 100, message = "SSH username must not exceed 100 characters") - private String sshUsername; + // SSH configuration fields (mapped from JobSubmissionInterface -> SSHJobSubmission) + private Integer sshPort; // Optional - defaults to 22 if not specified + private String alternativeSSHHostName; // Optional alternative hostname + private String securityProtocol; // SSH_KEYS, USERNAME_PASSWORD (from SecurityProtocol enum) - @NotNull(message = "SSH port is required") - @Min(value = 1, message = "SSH port must be at least 1") - private Integer sshPort; - - @NotBlank(message = "Authentication method is required") - @Size(max = 50, message = "Authentication method must not exceed 50 characters") - private String authenticationMethod; // SSH_KEY or PASSWORD - - private String sshKey; // SSH key content for SSH_KEY authentication - - @NotBlank(message = "Working directory is required") - @Size(max = 500, message = "Working directory must not exceed 500 characters") - private String workingDirectory; - - @NotBlank(message = "Scheduler type is required") - @Size(max = 50, message = "Scheduler type must not exceed 50 characters") - private String schedulerType; // SLURM, PBS, SGE, etc. - - @NotBlank(message = "Data movement protocol is required") - @Size(max = 50, message = "Data movement protocol must not exceed 50 characters") - private String dataMovementProtocol; // SCP, SFTP, etc. + // Job management fields (mapped from JobSubmissionInterface -> SSHJobSubmission -> ResourceJobManager) + private String resourceJobManagerType; // PBS, SLURM, UGE, etc. (from ResourceJobManagerType enum) + + // Data movement fields (mapped from DataMovementInterface) + private String dataMovementProtocol; // SCP, SFTP, GRIDFTP, etc. (from DataMovementProtocol enum) // Queue management private List queues = new ArrayList<>(); @@ -229,12 +213,12 @@ public void setIpAddresses(List ipAddresses) { this.ipAddresses = ipAddresses; } - public String getSshUsername() { - return sshUsername; + public String getAlternativeSSHHostName() { + return alternativeSSHHostName; } - public void setSshUsername(String sshUsername) { - this.sshUsername = sshUsername; + public void setAlternativeSSHHostName(String alternativeSSHHostName) { + this.alternativeSSHHostName = alternativeSSHHostName; } public Integer getSshPort() { @@ -245,36 +229,20 @@ public void setSshPort(Integer sshPort) { this.sshPort = sshPort; } - public String getAuthenticationMethod() { - return authenticationMethod; - } - - public void setAuthenticationMethod(String authenticationMethod) { - this.authenticationMethod = authenticationMethod; - } - - public String getSshKey() { - return sshKey; - } - - public void setSshKey(String sshKey) { - this.sshKey = sshKey; - } - - public String getWorkingDirectory() { - return workingDirectory; + public String getSecurityProtocol() { + return securityProtocol; } - public void setWorkingDirectory(String workingDirectory) { - this.workingDirectory = workingDirectory; + public void setSecurityProtocol(String securityProtocol) { + this.securityProtocol = securityProtocol; } - public String getSchedulerType() { - return schedulerType; + public String getResourceJobManagerType() { + return resourceJobManagerType; } - public void setSchedulerType(String schedulerType) { - this.schedulerType = schedulerType; + public void setResourceJobManagerType(String resourceJobManagerType) { + this.resourceJobManagerType = resourceJobManagerType; } public String getDataMovementProtocol() { diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/ComputeResourceEntity.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/ComputeResourceEntity.java deleted file mode 100644 index 9ba4a035889..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/ComputeResourceEntity.java +++ /dev/null @@ -1,168 +0,0 @@ -package org.apache.airavata.research.service.entity; - -import jakarta.persistence.*; -import java.io.Serializable; -import java.sql.Timestamp; - -@Entity -@Table(name = "COMPUTE_RESOURCE") -public class ComputeResourceEntity implements Serializable { - private static final long serialVersionUID = 1L; - - @Id - @Column(name = "RESOURCE_ID") - private String resourceId; - - @Column(name = "HOST_NAME", nullable = false) - private String hostName; - - - @Column(name = "RESOURCE_DESCRIPTION", length = 2048) - private String resourceDescription; - - @Column(name = "CREATION_TIME", nullable = false) - private Timestamp creationTime; - - @Column(name = "UPDATE_TIME", nullable = false) - private Timestamp updateTime; - - @Column(name = "MAX_MEMORY_NODE") - private Integer maxMemoryNode; - - @Column(name = "CPUS_PER_NODE") - private Integer cpusPerNode; - - @Column(name = "DEFAULT_NODE_COUNT") - private Integer defaultNodeCount; - - @Column(name = "DEFAULT_CPU_COUNT") - private Integer defaultCpuCount; - - @Column(name = "DEFAULT_WALLTIME") - private Integer defaultWalltime; - - @Column(name = "ENABLED") - private Short enabled; - - @Column(name = "GATEWAY_USAGE_REPORTING") - private Boolean gatewayUsageReporting; - - @Column(name = "GATEWAY_USAGE_MODULE_LOAD_CMD", length = 500) - private String gatewayUsageModuleLoadCmd; - - @Column(name = "GATEWAY_USAGE_EXECUTABLE") - private String gatewayUsageExecutable; - - public String getResourceId() { - return resourceId; - } - - public void setResourceId(String resourceId) { - this.resourceId = resourceId; - } - - public String getHostName() { - return hostName; - } - - public void setHostName(String hostName) { - this.hostName = hostName; - } - - - public String getResourceDescription() { - return resourceDescription; - } - - public void setResourceDescription(String resourceDescription) { - this.resourceDescription = resourceDescription; - } - - public Timestamp getCreationTime() { - return creationTime; - } - - public void setCreationTime(Timestamp creationTime) { - this.creationTime = creationTime; - } - - public Timestamp getUpdateTime() { - return updateTime; - } - - public void setUpdateTime(Timestamp updateTime) { - this.updateTime = updateTime; - } - - public Integer getMaxMemoryNode() { - return maxMemoryNode; - } - - public void setMaxMemoryNode(Integer maxMemoryNode) { - this.maxMemoryNode = maxMemoryNode; - } - - public Integer getCpusPerNode() { - return cpusPerNode; - } - - public void setCpusPerNode(Integer cpusPerNode) { - this.cpusPerNode = cpusPerNode; - } - - public Integer getDefaultNodeCount() { - return defaultNodeCount; - } - - public void setDefaultNodeCount(Integer defaultNodeCount) { - this.defaultNodeCount = defaultNodeCount; - } - - public Integer getDefaultCpuCount() { - return defaultCpuCount; - } - - public void setDefaultCpuCount(Integer defaultCpuCount) { - this.defaultCpuCount = defaultCpuCount; - } - - public Integer getDefaultWalltime() { - return defaultWalltime; - } - - public void setDefaultWalltime(Integer defaultWalltime) { - this.defaultWalltime = defaultWalltime; - } - - public Short getEnabled() { - return enabled; - } - - public void setEnabled(Short enabled) { - this.enabled = enabled; - } - - public Boolean getGatewayUsageReporting() { - return gatewayUsageReporting; - } - - public void setGatewayUsageReporting(Boolean gatewayUsageReporting) { - this.gatewayUsageReporting = gatewayUsageReporting; - } - - public String getGatewayUsageModuleLoadCmd() { - return gatewayUsageModuleLoadCmd; - } - - public void setGatewayUsageModuleLoadCmd(String gatewayUsageModuleLoadCmd) { - this.gatewayUsageModuleLoadCmd = gatewayUsageModuleLoadCmd; - } - - public String getGatewayUsageExecutable() { - return gatewayUsageExecutable; - } - - public void setGatewayUsageExecutable(String gatewayUsageExecutable) { - this.gatewayUsageExecutable = gatewayUsageExecutable; - } -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/StorageResourceEntity.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/StorageResourceEntity.java deleted file mode 100644 index eefa4c6c0ee..00000000000 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/entity/StorageResourceEntity.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.apache.airavata.research.service.entity; - -import jakarta.persistence.*; -import java.io.Serializable; -import java.sql.Timestamp; - -@Entity -@Table(name = "STORAGE_RESOURCE") -public class StorageResourceEntity implements Serializable { - private static final long serialVersionUID = 1L; - - @Id - @Column(name = "STORAGE_RESOURCE_ID") - private String storageResourceId; - - @Column(name = "HOST_NAME", nullable = false) - private String hostName; - - - @Column(name = "DESCRIPTION", length = 2048) - private String description; - - @Column(name = "ENABLED") - private Short enabled; - - @Column(name = "CREATION_TIME", nullable = false) - private Timestamp creationTime; - - @Column(name = "UPDATE_TIME", nullable = false) - private Timestamp updateTime; - - public String getStorageResourceId() { - return storageResourceId; - } - - public void setStorageResourceId(String storageResourceId) { - this.storageResourceId = storageResourceId; - } - - public String getHostName() { - return hostName; - } - - public void setHostName(String hostName) { - this.hostName = hostName; - } - - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public Short getEnabled() { - return enabled; - } - - public void setEnabled(Short enabled) { - this.enabled = enabled; - } - - public Timestamp getCreationTime() { - return creationTime; - } - - public void setCreationTime(Timestamp creationTime) { - this.creationTime = creationTime; - } - - public Timestamp getUpdateTime() { - return updateTime; - } - - public void setUpdateTime(Timestamp updateTime) { - this.updateTime = updateTime; - } -} \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/ComputeResourceHandler.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/ComputeResourceHandler.java index 1b36101eeed..0d38214d599 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/ComputeResourceHandler.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/ComputeResourceHandler.java @@ -18,7 +18,7 @@ */ package org.apache.airavata.research.service.handler; -import org.apache.airavata.research.service.entity.ComputeResourceEntity; +import org.apache.airavata.registry.core.entities.appcatalog.ComputeResourceEntity; import org.apache.airavata.research.service.dto.ComputeResourceDTO; import org.apache.airavata.research.service.repository.ComputeResourceRepository; import org.apache.airavata.research.service.util.DTOConverter; @@ -138,7 +138,7 @@ public ComputeResourceDTO createComputeResource(ComputeResourceDTO computeResour ComputeResourceEntity entity = dtoConverter.computeResourceDTOToEntity(computeResourceDTO); // Set system fields - entity.setResourceId(UUID.randomUUID().toString()); + entity.setComputeResourceId(UUID.randomUUID().toString()); entity.setEnabled((short) 1); entity.setCreationTime(new Timestamp(System.currentTimeMillis())); entity.setUpdateTime(new Timestamp(System.currentTimeMillis())); @@ -149,7 +149,7 @@ public ComputeResourceDTO createComputeResource(ComputeResourceDTO computeResour // Convert back to DTO ComputeResourceDTO savedDTO = dtoConverter.computeEntityToDTO(savedEntity); - LOGGER.info("Created compute resource in app_catalog with ID: {}", savedEntity.getResourceId()); + LOGGER.info("Created compute resource in app_catalog with ID: {}", savedEntity.getComputeResourceId()); return savedDTO; } catch (Exception e) { LOGGER.error("Failed to create compute resource in app_catalog", e); @@ -175,7 +175,7 @@ public ComputeResourceDTO updateComputeResource(String computeResourceId, Comput // Preserve system fields ComputeResourceEntity existing = existingOpt.get(); - updatedEntity.setResourceId(computeResourceId); + updatedEntity.setComputeResourceId(computeResourceId); updatedEntity.setCreationTime(existing.getCreationTime()); updatedEntity.setUpdateTime(new Timestamp(System.currentTimeMillis())); diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/StorageResourceHandler.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/StorageResourceHandler.java index c7731be812b..8d6b26fc118 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/StorageResourceHandler.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handler/StorageResourceHandler.java @@ -18,7 +18,7 @@ */ package org.apache.airavata.research.service.handler; -import org.apache.airavata.research.service.entity.StorageResourceEntity; +import org.apache.airavata.registry.core.entities.appcatalog.StorageResourceEntity; import org.apache.airavata.research.service.dto.StorageResourceDTO; import org.apache.airavata.research.service.repository.StorageResourceRepository; import org.apache.airavata.research.service.util.DTOConverter; @@ -140,7 +140,7 @@ public StorageResourceDTO createStorageResource(StorageResourceDTO storageResour // Set system fields entity.setStorageResourceId(UUID.randomUUID().toString()); - entity.setEnabled((short) 1); + entity.setEnabled(true); entity.setCreationTime(new Timestamp(System.currentTimeMillis())); entity.setUpdateTime(new Timestamp(System.currentTimeMillis())); diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/ComputeResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/ComputeResourceRepository.java index 499bb734aef..b2db89c8622 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/ComputeResourceRepository.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/ComputeResourceRepository.java @@ -18,7 +18,7 @@ */ package org.apache.airavata.research.service.repository; -import org.apache.airavata.research.service.entity.ComputeResourceEntity; +import org.apache.airavata.registry.core.entities.appcatalog.ComputeResourceEntity; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/StorageResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/StorageResourceRepository.java index 58f908c139b..673bea64f87 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/StorageResourceRepository.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/repository/StorageResourceRepository.java @@ -18,7 +18,7 @@ */ package org.apache.airavata.research.service.repository; -import org.apache.airavata.research.service.entity.StorageResourceEntity; +import org.apache.airavata.registry.core.entities.appcatalog.StorageResourceEntity; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -29,9 +29,9 @@ @Repository public interface StorageResourceRepository extends JpaRepository { - @Query("SELECT s FROM StorageResourceEntity s WHERE s.enabled = 1 ORDER BY s.creationTime DESC") + @Query("SELECT s FROM StorageResourceEntity s WHERE s.enabled = true ORDER BY s.creationTime DESC") List findAllEnabledOrderByCreationTime(); - @Query("SELECT s FROM StorageResourceEntity s WHERE s.enabled = 1 AND s.hostName LIKE %:hostname%") + @Query("SELECT s FROM StorageResourceEntity s WHERE s.enabled = true AND s.hostName LIKE %:hostname%") List findEnabledByHostNameContaining(@Param("hostname") String hostname); } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java index 5bb442efd76..ea45ebaea82 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/util/DTOConverter.java @@ -25,12 +25,19 @@ import java.util.stream.Collectors; import org.apache.airavata.model.appcatalog.computeresource.BatchQueue; import org.apache.airavata.model.appcatalog.computeresource.ComputeResourceDescription; +import org.apache.airavata.model.appcatalog.computeresource.JobSubmissionInterface; +import org.apache.airavata.model.data.movement.DataMovementInterface; +import org.apache.airavata.model.appcatalog.computeresource.JobSubmissionProtocol; +import org.apache.airavata.model.data.movement.DataMovementProtocol; import org.apache.airavata.model.appcatalog.storageresource.StorageResourceDescription; import org.apache.airavata.research.service.dto.ComputeResourceDTO; import org.apache.airavata.research.service.dto.ComputeResourceQueueDTO; import org.apache.airavata.research.service.dto.StorageResourceDTO; -import org.apache.airavata.research.service.entity.ComputeResourceEntity; -import org.apache.airavata.research.service.entity.StorageResourceEntity; +import org.apache.airavata.registry.core.entities.appcatalog.ComputeResourceEntity; +import org.apache.airavata.registry.core.entities.appcatalog.StorageResourceEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; /** @@ -118,6 +125,24 @@ public ComputeResourceDTO thriftToDTO(ComputeResourceDescription thriftModel) { .collect(Collectors.toList())); } + // Extract data movement protocol from DataMovementInterface + if (thriftModel.getDataMovementInterfaces() != null && !thriftModel.getDataMovementInterfaces().isEmpty()) { + // Get the first (highest priority) data movement interface + DataMovementInterface dmInterface = thriftModel.getDataMovementInterfaces().get(0); + if (dmInterface != null && dmInterface.getDataMovementProtocol() != null) { + dto.setDataMovementProtocol(dmInterface.getDataMovementProtocol().toString()); + } + } + + // Extract resource job manager type from JobSubmissionInterface + if (thriftModel.getJobSubmissionInterfaces() != null && !thriftModel.getJobSubmissionInterfaces().isEmpty()) { + // Get the first (highest priority) job submission interface + JobSubmissionInterface jsInterface = thriftModel.getJobSubmissionInterfaces().get(0); + if (jsInterface != null && jsInterface.getJobSubmissionProtocol() != null) { + dto.setResourceJobManagerType(jsInterface.getJobSubmissionProtocol().toString()); + } + } + return dto; } @@ -158,6 +183,36 @@ public ComputeResourceDescription dtoToThrift(ComputeResourceDTO dto) { .collect(Collectors.toList())); } + // Create DataMovementInterface if protocol is specified + if (dto.getDataMovementProtocol() != null && !dto.getDataMovementProtocol().trim().isEmpty()) { + List dataMovementInterfaces = new ArrayList<>(); + DataMovementInterface dmInterface = new DataMovementInterface(); + dmInterface.setDataMovementInterfaceId(generateInterfaceId("dm")); + dmInterface.setPriorityOrder(1); // Highest priority + try { + dmInterface.setDataMovementProtocol(DataMovementProtocol.valueOf(dto.getDataMovementProtocol())); + dataMovementInterfaces.add(dmInterface); + thriftModel.setDataMovementInterfaces(dataMovementInterfaces); + } catch (IllegalArgumentException e) { + LOGGER.warn("Invalid data movement protocol: " + dto.getDataMovementProtocol(), e); + } + } + + // Create JobSubmissionInterface if protocol is specified + if (dto.getResourceJobManagerType() != null && !dto.getResourceJobManagerType().trim().isEmpty()) { + List jobSubmissionInterfaces = new ArrayList<>(); + JobSubmissionInterface jsInterface = new JobSubmissionInterface(); + jsInterface.setJobSubmissionInterfaceId(generateInterfaceId("js")); + jsInterface.setPriorityOrder(1); // Highest priority + try { + jsInterface.setJobSubmissionProtocol(JobSubmissionProtocol.valueOf(dto.getResourceJobManagerType())); + jobSubmissionInterfaces.add(jsInterface); + thriftModel.setJobSubmissionInterfaces(jobSubmissionInterfaces); + } catch (IllegalArgumentException e) { + LOGGER.warn("Invalid job submission protocol: " + dto.getResourceJobManagerType(), e); + } + } + return thriftModel; } @@ -276,17 +331,15 @@ private void parseResourceDescriptionForComputeResource(String resourceDescripti dto.setQueueSystem(getStringValue(uiFieldsNode, QUEUE_SYSTEM_KEY)); dto.setAdditionalInfo(getStringValue(uiFieldsNode, ADDITIONAL_INFO_KEY)); dto.setResourceManager(getStringValue(uiFieldsNode, RESOURCE_MANAGER_KEY)); - dto.setWorkingDirectory(getStringValue(uiFieldsNode, WORKING_DIR_KEY)); - dto.setSchedulerType(getStringValue(uiFieldsNode, SCHEDULER_TYPE_KEY)); + dto.setResourceJobManagerType(getStringValue(uiFieldsNode, SCHEDULER_TYPE_KEY)); // Map old schedulerType to new field dto.setDataMovementProtocol(getStringValue(uiFieldsNode, DATA_MOVEMENT_PROTOCOL_KEY)); // Extract SSH configuration JsonNode sshConfigNode = uiFieldsNode.get(SSH_CONFIG_KEY); if (sshConfigNode != null) { - dto.setSshUsername(getStringValue(sshConfigNode, SSH_USERNAME_KEY)); + dto.setAlternativeSSHHostName(getStringValue(sshConfigNode, SSH_USERNAME_KEY)); // Repurpose for alternative hostname dto.setSshPort(getIntegerValue(sshConfigNode, SSH_PORT_KEY)); - dto.setAuthenticationMethod(getStringValue(sshConfigNode, AUTH_METHOD_KEY)); - dto.setSshKey(getStringValue(sshConfigNode, SSH_KEY_KEY)); + dto.setSecurityProtocol(getStringValue(sshConfigNode, AUTH_METHOD_KEY)); // Map to securityProtocol } } @@ -320,16 +373,14 @@ private String buildResourceDescriptionForComputeResource(ComputeResourceDTO dto uiFields.put(QUEUE_SYSTEM_KEY, dto.getQueueSystem()); uiFields.put(ADDITIONAL_INFO_KEY, dto.getAdditionalInfo()); uiFields.put(RESOURCE_MANAGER_KEY, dto.getResourceManager()); - uiFields.put(WORKING_DIR_KEY, dto.getWorkingDirectory()); - uiFields.put(SCHEDULER_TYPE_KEY, dto.getSchedulerType()); + uiFields.put(SCHEDULER_TYPE_KEY, dto.getResourceJobManagerType()); // Use new field name uiFields.put(DATA_MOVEMENT_PROTOCOL_KEY, dto.getDataMovementProtocol()); // SSH configuration Map sshConfig = new HashMap<>(); - sshConfig.put(SSH_USERNAME_KEY, dto.getSshUsername()); + sshConfig.put(SSH_USERNAME_KEY, dto.getAlternativeSSHHostName()); // Repurposed field sshConfig.put(SSH_PORT_KEY, dto.getSshPort()); - sshConfig.put(AUTH_METHOD_KEY, dto.getAuthenticationMethod()); - sshConfig.put(SSH_KEY_KEY, dto.getSshKey()); + sshConfig.put(AUTH_METHOD_KEY, dto.getSecurityProtocol()); // Use new field name uiFields.put(SSH_CONFIG_KEY, sshConfig); rootMap.put(UI_FIELDS_KEY, uiFields); @@ -486,18 +537,17 @@ public StorageResourceDTO storageEntityToDTO(StorageResourceEntity entity) { // Core fields dto.setStorageResourceId(entity.getStorageResourceId()); dto.setHostName(entity.getHostName()); - dto.setStorageResourceDescription(entity.getDescription()); - // Handle enabled field safely - Short type from database - Short enabledValue = entity.getEnabled(); - dto.setEnabled(enabledValue != null && enabledValue.shortValue() == 1); + dto.setStorageResourceDescription(entity.getStorageResourceDescription()); + // Handle enabled field - boolean type from database + dto.setEnabled(entity.isEnabled()); // Extract name from UI fields or generate fallback - String extractedName = extractNameFromStorageDescription(entity.getDescription()); + String extractedName = extractNameFromStorageDescription(entity.getStorageResourceDescription()); if (extractedName != null && !extractedName.trim().isEmpty()) { dto.setName(extractedName); } else { // Generate name from hostname and description - dto.setName(generateStorageResourceName(entity.getHostName(), entity.getDescription())); + dto.setName(generateStorageResourceName(entity.getHostName(), entity.getStorageResourceDescription())); } // Timestamps @@ -508,8 +558,8 @@ public StorageResourceDTO storageEntityToDTO(StorageResourceEntity entity) { dto.setUpdateTime(entity.getUpdateTime().getTime()); } - // Extract UI-specific fields from description JSON - extractStorageUIFieldsFromDescription(entity.getDescription(), dto); + // Extract UI-specific fields from JSON stored in description + extractStorageUIFieldsFromDescription(entity.getStorageResourceDescription(), dto); return dto; } @@ -527,11 +577,10 @@ public StorageResourceEntity storageResourceDTOToEntity(StorageResourceDTO dto) // Core fields entity.setStorageResourceId(dto.getStorageResourceId()); entity.setHostName(dto.getHostName()); - entity.setEnabled(dto.isEnabled() ? (short) 1 : (short) 0); + entity.setEnabled(dto.isEnabled()); - // Embed UI fields into description as JSON - String descriptionWithUIFields = encodeStorageUIFieldsIntoDescription(dto); - entity.setDescription(descriptionWithUIFields); + // Encode UI-specific fields into JSON within description + entity.setStorageResourceDescription(encodeStorageUIFieldsIntoDescription(dto)); return entity; } @@ -547,14 +596,14 @@ public ComputeResourceDTO computeEntityToDTO(ComputeResourceEntity entity) { ComputeResourceDTO dto = new ComputeResourceDTO(); // Core fields - dto.setComputeResourceId(entity.getResourceId()); + dto.setComputeResourceId(entity.getComputeResourceId()); dto.setHostName(entity.getHostName()); dto.setResourceDescription(entity.getResourceDescription()); // Handle enabled field safely - Short type from database Short enabledValue = entity.getEnabled(); dto.setEnabled(enabledValue != null && enabledValue.shortValue() == 1); dto.setCpuCores(entity.getCpusPerNode()); - dto.setMemoryGB(entity.getMaxMemoryNode()); + dto.setMemoryGB(entity.getMaxMemoryPerNode()); // Extract name from UI fields or generate fallback String extractedName = extractNameFromDescription(entity.getResourceDescription()); @@ -573,7 +622,7 @@ public ComputeResourceDTO computeEntityToDTO(ComputeResourceEntity entity) { dto.setUpdateTime(entity.getUpdateTime().getTime()); } - // Extract UI-specific fields from description JSON + // Extract UI-specific fields from JSON stored in description extractComputeUIFieldsFromDescription(entity.getResourceDescription(), dto); // Initialize empty arrays for fields not stored in database @@ -601,15 +650,14 @@ public ComputeResourceEntity computeResourceDTOToEntity(ComputeResourceDTO dto) ComputeResourceEntity entity = new ComputeResourceEntity(); // Core fields - entity.setResourceId(dto.getComputeResourceId()); + entity.setComputeResourceId(dto.getComputeResourceId()); entity.setHostName(dto.getHostName()); - entity.setEnabled(dto.isEnabled() ? (short) 1 : (short) 0); + entity.setEnabled(dto.isEnabled() ? Short.valueOf((short) 1) : Short.valueOf((short) 0)); entity.setCpusPerNode(dto.getCpuCores()); - entity.setMaxMemoryNode(dto.getMemoryGB()); + entity.setMaxMemoryPerNode(dto.getMemoryGB()); - // Embed UI fields into description as JSON - String descriptionWithUIFields = encodeComputeUIFieldsIntoDescription(dto); - entity.setResourceDescription(descriptionWithUIFields); + // Encode UI-specific fields into JSON within description + entity.setResourceDescription(encodeComputeUIFieldsIntoDescription(dto)); return entity; } @@ -672,17 +720,15 @@ private void extractComputeUIFieldsFromDescription(String description, ComputeRe // Extract UI-specific fields dto.setComputeType(getStringValue(rootNode, COMPUTE_TYPE_KEY)); dto.setOperatingSystem(getStringValue(rootNode, OPERATING_SYSTEM_KEY)); - dto.setSchedulerType(getStringValue(rootNode, SCHEDULER_TYPE_KEY)); + dto.setResourceJobManagerType(getStringValue(rootNode, SCHEDULER_TYPE_KEY)); // Map old schedulerType to new field dto.setDataMovementProtocol(getStringValue(rootNode, DATA_MOVEMENT_PROTOCOL_KEY)); dto.setQueueSystem(getStringValue(rootNode, QUEUE_SYSTEM_KEY)); dto.setResourceManager(getStringValue(rootNode, RESOURCE_MANAGER_KEY)); - dto.setWorkingDirectory(getStringValue(rootNode, WORKING_DIR_KEY)); - // Extract SSH fields - dto.setSshUsername(getStringValue(rootNode, SSH_USERNAME_KEY)); + // Extract SSH fields (updated field names) dto.setSshPort(getIntegerValue(rootNode, SSH_PORT_KEY)); - dto.setAuthenticationMethod(getStringValue(rootNode, AUTH_METHOD_KEY)); - dto.setSshKey(getStringValue(rootNode, SSH_KEY_KEY)); + dto.setSecurityProtocol(getStringValue(rootNode, AUTH_METHOD_KEY)); // Map authenticationMethod to securityProtocol + dto.setAlternativeSSHHostName(getStringValue(rootNode, SSH_USERNAME_KEY)); // Repurpose for alternative hostname // Extract preserved fields dto.setName(getStringValue(rootNode, NAME_KEY)); @@ -804,17 +850,15 @@ private String encodeComputeUIFieldsIntoDescription(ComputeResourceDTO dto) { Map uiFields = new HashMap<>(); uiFields.put(COMPUTE_TYPE_KEY, dto.getComputeType()); uiFields.put(OPERATING_SYSTEM_KEY, dto.getOperatingSystem()); - uiFields.put(SCHEDULER_TYPE_KEY, dto.getSchedulerType()); + uiFields.put(SCHEDULER_TYPE_KEY, dto.getResourceJobManagerType()); // Use new field name uiFields.put(DATA_MOVEMENT_PROTOCOL_KEY, dto.getDataMovementProtocol()); uiFields.put(QUEUE_SYSTEM_KEY, dto.getQueueSystem()); uiFields.put(RESOURCE_MANAGER_KEY, dto.getResourceManager()); - uiFields.put(WORKING_DIR_KEY, dto.getWorkingDirectory()); - // SSH configuration fields - uiFields.put(SSH_USERNAME_KEY, dto.getSshUsername()); + // SSH configuration fields (updated field names) + uiFields.put(SSH_USERNAME_KEY, dto.getAlternativeSSHHostName()); // Repurposed field uiFields.put(SSH_PORT_KEY, dto.getSshPort()); - uiFields.put(AUTH_METHOD_KEY, dto.getAuthenticationMethod()); - uiFields.put(SSH_KEY_KEY, dto.getSshKey()); + uiFields.put(AUTH_METHOD_KEY, dto.getSecurityProtocol()); // Use new field name // Preserve critical fields that might be lost uiFields.put(NAME_KEY, dto.getName()); @@ -948,4 +992,11 @@ private String extractNameFromStorageDescription(String description) { return null; } } + + /** + * Generate a unique interface ID for JobSubmissionInterface or DataMovementInterface + */ + private String generateInterfaceId(String prefix) { + return prefix + "_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8); + } } \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java index 31a49f98a2a..38936873429 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/ComputeResourceController.java @@ -117,8 +117,7 @@ public ResponseEntity createComputeResource(@Valid @RequestBody ComputeResour return ResponseEntity.badRequest().body("Validation failed: " + errorMessage); } - // Set intelligent defaults for fields not provided by UI - setDefaultValues(computeResourceDTO); + // TODO: Remove setDefaultValues() as part of migration - rely on DTO validation instead try { @@ -153,8 +152,7 @@ public ResponseEntity updateComputeResource(@PathVariable("id") String id, @V return ResponseEntity.badRequest().body("Validation failed: " + errorMessage); } - // Set intelligent defaults for fields not provided by UI - setDefaultValues(computeResourceDTO); + // TODO: Remove setDefaultValues() as part of migration - rely on DTO validation instead try { ComputeResourceDTO updatedResource = computeResourceHandler.updateComputeResource(id, computeResourceDTO); @@ -318,27 +316,24 @@ private void setDefaultValues(ComputeResourceDTO dto) { dto.setResourceManager("Default Resource Manager"); } - // Set default SSH configuration - if (dto.getSshUsername() == null || dto.getSshUsername().trim().isEmpty()) { - dto.setSshUsername("admin"); + // Set default SSH configuration (using alternative hostname field) + if (dto.getAlternativeSSHHostName() == null || dto.getAlternativeSSHHostName().trim().isEmpty()) { + dto.setAlternativeSSHHostName(dto.getHostName()); // Default to main hostname } if (dto.getSshPort() == null) { dto.setSshPort(22); } - if (dto.getAuthenticationMethod() == null || dto.getAuthenticationMethod().trim().isEmpty()) { - dto.setAuthenticationMethod("SSH_KEYS"); + if (dto.getSecurityProtocol() == null || dto.getSecurityProtocol().trim().isEmpty()) { + dto.setSecurityProtocol("SSH_KEYS"); } - // Set default working directory - if (dto.getWorkingDirectory() == null || dto.getWorkingDirectory().trim().isEmpty()) { - dto.setWorkingDirectory("/tmp"); - } + // Working directory is no longer a direct field - handled by related entities - // Set default scheduler type - if (dto.getSchedulerType() == null || dto.getSchedulerType().trim().isEmpty()) { - dto.setSchedulerType("SLURM"); + // Set default resource job manager type + if (dto.getResourceJobManagerType() == null || dto.getResourceJobManagerType().trim().isEmpty()) { + dto.setResourceJobManagerType("SLURM"); } // Set default data movement protocol diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java index 3babdb1c18c..5da616a8c2f 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/v2/controller/StorageResourceController.java @@ -117,7 +117,7 @@ public ResponseEntity createStorageResource(@Valid @RequestBody StorageResour } // Set intelligent defaults for fields not provided by UI - setDefaultValues(storageResourceDTO); + // TODO: Remove setDefaultValues() as part of migration - rely on DTO validation instead try { @@ -148,7 +148,7 @@ public ResponseEntity updateStorageResource(@PathVariable("id") String id, @V } // Set intelligent defaults for fields not provided by UI - setDefaultValues(storageResourceDTO); + // TODO: Remove setDefaultValues() as part of migration - rely on DTO validation instead try { StorageResourceDTO updatedResource = storageResourceHandler.updateStorageResource(id, storageResourceDTO); From d38f217b1cfa9f3b0016f232c9331bf91e648bfc Mon Sep 17 00:00:00 2001 From: Krish Katariya Date: Fri, 8 Aug 2025 17:58:51 -0700 Subject: [PATCH 15/17] Readme Update --- .../research-service/README.md | 42 ++++++------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/modules/research-framework/research-service/README.md b/modules/research-framework/research-service/README.md index 32fc72d1e98..42be6983c01 100644 --- a/modules/research-framework/research-service/README.md +++ b/modules/research-framework/research-service/README.md @@ -26,7 +26,7 @@ A comprehensive Spring Boot REST API service for managing research resources, co The Research Service employs a **dual database architecture** designed to separate research data from infrastructure management: - **H2 Database (In-Memory)**: Manages v1 research resources (Projects, Datasets, Models, Notebooks, Repositories) -- **MariaDB Database**: Manages v2 infrastructure resources (Compute Resources, Storage Resources) +- **MariaDB Database**: Manages v2 infrastructure resources (Compute Resources, Storage Resources) using imported airavata-api entities - **RESTful API**: Comprehensive v1 and v2 endpoints with different authentication requirements - **Multi-Profile Configuration**: Supports development and production environments @@ -70,7 +70,9 @@ docker-compose -f .devcontainer/docker-compose.yml up db adminer ```bash cd airavata/modules/research-framework/research-service -# Run column length migration (required for enhanced JSON storage) +# Run column length migration (REQUIRED for UI field JSON storage) +# This migration increases column lengths in airavata-api entities to support +# JSON serialization of UI-specific fields like queues, hostAliases, etc. mysql -h airavata.host -P 13306 -u airavata -p123456 app_catalog < database-migrations/001-increase-description-column-lengths.sql ``` @@ -105,15 +107,14 @@ mvn spring-boot:run #### MariaDB Database (v2 Infrastructure) - **Purpose**: Production infrastructure and computational resources - **Location**: `airavata.host:13306/app_catalog` -- **Entities**: `ComputeResourceEntity`, `StorageResourceEntity`, `Code` -- **Resource Types**: HPC clusters, storage systems, research codes +- **Entities**: `ComputeResourceEntity`, `StorageResourceEntity` (imported from airavata-api) +- **Resource Types**: HPC clusters, supercomputers, cloud resources, storage systems - **Sample Data**: 12+ infrastructure resources ### Data Initializers - **`DatasetInitializer`**: Creates 9 research datasets (all profiles) - **`DevDataInitializer`**: Creates 10 neuroscience projects with full resource sets (dev profile only) -- **`V2DataInitializer`**: Creates 11 code resources and samples (all profiles) ## 🔐 Authentication @@ -175,25 +176,6 @@ curl -X POST http://localhost:8080/api/dev/auth/token \ ### V2 API - Infrastructure Resources (MariaDB) -#### Codes (`/api/v2/rf/codes`) 🔒 -- `GET /` - List codes (with pagination, filtering) -- `GET /{id}` - Get code by ID -- `POST /` - Create code resource -- `PUT /{id}` - Update code resource -- `DELETE /{id}` - Delete code resource -- `GET /search` - Search by keyword -- `GET /type/{codeType}` - Filter by code type -- `GET /language/{language}` - Filter by programming language -- `GET /framework/{framework}` - Filter by framework -- `GET /tag/{tag}` - Filter by tag -- `GET /author/{author}` - Filter by author -- `GET /top-starred` - Get most starred codes -- `GET /recent` - Get recent codes -- `POST /{id}/star` - Star/unstar code -- `GET /{id}/star` - Check star status -- `GET /{id}/stars/count` - Get star count -- `GET /starred` - Get starred codes - #### Compute Resources (`/api/v2/rf/compute-resources`) 🔒 - `GET /` - List compute resources (with name search) - `GET /{id}` - Get compute resource by ID @@ -242,12 +224,13 @@ Headers: X-API-Key: dev-research-api-key-12345 "operatingSystem": "Cray Linux Environment", "hostAliases": ["titan-login1.supercluster.edu"], "ipAddresses": ["128.219.10.1"], - "sshUsername": "user123", "sshPort": 22, - "authenticationMethod": "SSH_KEY", - "workingDirectory": "/lustre/home/user123", - "schedulerType": "SLURM", + "alternativeSSHHostName": "titan-login.supercluster.edu", + "securityProtocol": "SSH_KEYS", + "resourceJobManagerType": "SLURM", "dataMovementProtocol": "SCP", + "queueSystem": "SLURM", + "resourceManager": "XSEDE", "queues": [ { "queueName": "default", @@ -271,13 +254,14 @@ Headers: X-API-Key: dev-research-api-key-12345 "storageResourceDescription": "AWS S3 bucket for research data", "storageType": "S3", "capacityTB": 1000, - "accessProtocol": "HTTPS", + "accessProtocol": "S3", "endpoint": "https://s3.amazonaws.com", "supportsEncryption": true, "supportsVersioning": true, "bucketName": "my-research-bucket", "accessKey": "AKIAIOSFODNN7EXAMPLE", "secretKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "resourceManager": "AWS", "enabled": true } ``` From 6cc075ada9454d7e9256072e0c444f280015bc7e Mon Sep 17 00:00:00 2001 From: Krish Katariya Date: Wed, 20 Aug 2025 18:03:36 -0700 Subject: [PATCH 16/17] Updates --- .../research-service/README.md | 90 +++++++++++++++++++ .../service/config/SecurityConfig.java | 9 +- .../service/handlers/ResourceHandler.java | 5 +- .../src/main/resources/application.yml | 5 +- 4 files changed, 103 insertions(+), 6 deletions(-) diff --git a/modules/research-framework/research-service/README.md b/modules/research-framework/research-service/README.md index 42be6983c01..0102ef6c6a8 100644 --- a/modules/research-framework/research-service/README.md +++ b/modules/research-framework/research-service/README.md @@ -357,4 +357,94 @@ When making significant changes to the Research Service: --- +## 📋 UI vs Database Field Mapping + +### Compute Resources + +#### UI Fields (Frontend Forms) +- `name` - Resource display name +- `resourceDescription` - Resource description +- `hostName` - Primary hostname +- `computeType` - Type of compute resource (HPC, Cloud, etc.) +- `cpuCores` - Number of CPU cores +- `memoryGB` - Memory in gigabytes +- `operatingSystem` - Operating system +- `hostAliases` - Array of alternative hostnames +- `ipAddresses` - Array of IP addresses +- `sshPort` - SSH port number +- `alternativeSSHHostName` - Alternative SSH hostname +- `securityProtocol` - Security protocol (SSH_KEYS, etc.) +- `resourceJobManagerType` - Job manager type (SLURM, PBS, etc.) +- `dataMovementProtocol` - Data movement protocol (SCP, SFTP, etc.) +- `queueSystem` - Queue system type +- `resourceManager` - Resource manager name +- `queues` - Array of queue configurations with maxNodes, maxProcessors, maxRunTime +- `enabled` - Boolean status + +#### App Catalog Database Fields (airavata-api entities) +- `computeResourceId` - Primary key UUID +- `computeResourceDescription` - Resource description +- `hostName` - Primary hostname +- `hostAliases` - JSON array in description field +- `ipAddresses` - JSON array in description field +- `resourceJobManagerType` - From JobSubmissionInterface enum +- `gatewayUsageReporting` - Boolean flag +- `gatewayUsageModuleLoadCommand` - Command string +- `gatewayUsageExecutable` - Executable path +- `cpuCount` - CPU core count +- `nodeCount` - Node count +- `ppn` - Processes per node +- `maxRunTime` - Maximum runtime +- `memoryPerNode` - Memory per node +- `loginUserName` - Login username +- `scratchLocation` - Scratch directory +- `allocationProjectNumber` - Project allocation number +- `resourceSpecificCredentialStoreToken` - Credential token +- `usageReportingGatewayId` - Gateway ID for reporting +- `creationTime` - Timestamp +- `updateTime` - Timestamp + +### Storage Resources + +#### UI Fields (Frontend Forms) +- `name` - Storage resource name +- `hostName` - Storage hostname +- `storageResourceDescription` - Description +- `storageType` - Type (S3, SFTP, etc.) +- `capacityTB` - Storage capacity in TB +- `accessProtocol` - Access protocol +- `endpoint` - Storage endpoint URL +- `supportsEncryption` - Encryption support boolean +- `supportsVersioning` - Versioning support boolean +- `bucketName` - S3 bucket name (S3 specific) +- `accessKey` - Access key (S3 specific) +- `secretKey` - Secret key (S3 specific) +- `resourceManager` - Resource manager name +- `enabled` - Boolean status + +#### App Catalog Database Fields (airavata-api entities) +- `storageResourceId` - Primary key UUID +- `hostName` - Storage hostname +- `storageResourceDescription` - Description +- `enabled` - Boolean status +- `creationTime` - Timestamp +- `updateTime` - Timestamp +- DataMovementInterface relations for protocol-specific configurations +- Storage-specific fields stored in related entities and JSON serialization + +### Field Mapping Strategy + +**Current Implementation (Post-August 2025):** +- **Direct Field Mapping**: UI fields map directly to DTO fields which map to entity fields +- **No JSON Injection**: Complex UI fields (arrays, objects) are handled through proper entity relationships +- **Entity Relationships**: Uses airavata-api's JobSubmissionInterface, DataMovementInterface, BatchQueue entities +- **Description Field**: Used only for actual descriptions, no JSON serialization + +**Previous Implementation (Pre-August 2025):** +- **JSON-in-Description**: UI-specific fields were serialized as JSON in the description column +- **Field Mismatch**: UI used deprecated field names that didn't match backend entities +- **Workaround**: DTOConverter extracted/encoded JSON from description fields + +--- + **Apache Airavata Research Service** - Empowering scientific discovery through unified research resource management. \ No newline at end of file diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/SecurityConfig.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/SecurityConfig.java index e2aeb74b3e2..c149f6a96c9 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/SecurityConfig.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/SecurityConfig.java @@ -45,9 +45,12 @@ @EnableMethodSecurity public class SecurityConfig { - @Value("${research.auth.jwks-uri:https://auth.dev.cybershuttle.org/.well-known/jwks}") + @Value("${research.auth.jwks-uri:https://auth.cybershuttle.org/realms/default/protocol/openid-connect/certs}") private String jwksUri; + @Value("${research.auth.issuer-uri:https://auth.cybershuttle.org/realms/default}") + private String issuerUri; + @Value("${research.auth.dev-api-key:dev-research-api-key-12345}") private String devApiKey; @@ -83,7 +86,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @Bean public JwtDecoder jwtDecoder() { - return NimbusJwtDecoder.withJwkSetUri(jwksUri).build(); + return NimbusJwtDecoder.withJwkSetUri(jwksUri) + .jwsAlgorithm(org.springframework.security.oauth2.jose.jws.SignatureAlgorithm.RS256) + .build(); } @Bean diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java index d4bff347f42..bcaa72f1e23 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java @@ -81,8 +81,9 @@ public void initializeResource(Resource resource) { UserProfile fetchedUser = airavataService.getUserProfile(authorId); userSet.add(fetchedUser.getUserId()); } catch (Exception e) { - LOGGER.error("Error while fetching user profile with the userId: {}", authorId, e); - throw new EntityNotFoundException("Error while fetching user profile with the userId: " + authorId, e); + LOGGER.warn("User profile service unavailable for userId: {}. Using provided ID for development.", authorId); + // For development, skip user validation and use the provided author ID + userSet.add(authorId); } } diff --git a/modules/research-framework/research-service/src/main/resources/application.yml b/modules/research-framework/research-service/src/main/resources/application.yml index 8b1dde7bf28..691fcb0f555 100644 --- a/modules/research-framework/research-service/src/main/resources/application.yml +++ b/modules/research-framework/research-service/src/main/resources/application.yml @@ -40,7 +40,7 @@ airavata: url: http://airavata.host:5173 dev-url: http://airavata.host:5173 openid: - url: "http://airavata.host:18080/realms/default" + url: "https://auth.dev.cybershuttle.org/realms/default" user-profile: server: url: airavata.host @@ -92,7 +92,8 @@ springdoc: # Authentication Configuration research: auth: - jwks-uri: "https://auth.dev.cybershuttle.org/.well-known/jwks" + jwks-uri: "https://auth.dev.cybershuttle.org/realms/default/protocol/openid-connect/certs" + issuer-uri: "https://auth.dev.cybershuttle.org/realms/default" dev-api-key: "dev-research-api-key-12345" cors: allowed-origins: "http://localhost:5173,http://localhost:3000" From 4192eebb97a562161b83fa36b18a222c17abcbcc Mon Sep 17 00:00:00 2001 From: Krish Katariya Date: Fri, 22 Aug 2025 00:50:40 -0400 Subject: [PATCH 17/17] Auth Changes --- .../service/config/AuthzTokenFilter.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java index 3c50339af34..cf29195c958 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java @@ -26,6 +26,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Arrays; import java.util.Base64; import java.util.HashMap; import java.util.Map; @@ -107,8 +108,17 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse authzToken.setClaimsMap(claimsMap); UserContext.setAuthzToken(authzToken); - UserProfile userProfile = airavataService.getUserProfile( - authzToken, getClaim(authzToken, USERNAME_CLAIM), getClaim(authzToken, GATEWAY_CLAIM)); + // Create UserProfile from JWT claims directly (no external UserProfileService needed) + UserProfile userProfile = new UserProfile(); + userProfile.setUserId(getClaim(authzToken, USERNAME_CLAIM)); + userProfile.setGatewayId(getClaim(authzToken, GATEWAY_CLAIM)); + + // Set email from JWT if available + String email = getOptionalClaim(authzToken, "email"); + if (email != null) { + userProfile.setEmails(Arrays.asList(email)); + } + UserContext.setUser(userProfile); } catch (Exception e) { LOGGER.error("Invalid authorization data", e); @@ -185,4 +195,12 @@ private static String getClaim(AuthzToken authzToken, String claimId) { .orElseThrow(() -> new IllegalArgumentException("Missing '" + claimId + "' claim in the authentication token")); } + + private static String getOptionalClaim(AuthzToken authzToken, String claimId) { + return authzToken.getClaimsMap().entrySet().stream() + .filter(entry -> entry.getKey().equalsIgnoreCase(claimId)) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + } }