diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index f174713..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,2 +0,0 @@ -If being asked to set up Azure infrastructure for this app, use the tool `run_command_in_terminal` to run script `azure-setup.sh` in the project root in WSL environment. -If being asked to deploy this app to Azure, use the tool `run_command_in_terminal` to run script `deploy-to-azure.sh` in the project root in WSL environment. \ No newline at end of file diff --git a/.gitignore b/.gitignore index fb4d0fd..f28849c 100644 --- a/.gitignore +++ b/.gitignore @@ -254,13 +254,18 @@ PhotoAlbum/wwwroot/uploads/* # Maven target/ -*.jar + +# Package files (except lib directory dependencies) *.war *.ear *.zip *.tar.gz *.rar +# JAR files (but keep lib/ directory JARs for Ant project) +*.jar +!lib/*.jar + # IDE .idea/ *.iws diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 8cfce58..0000000 --- a/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -# Use Maven with OpenJDK 8 for building -FROM maven:3.9.6-eclipse-temurin-8 AS build - -WORKDIR /app - -# Copy Maven files for dependency resolution -COPY pom.xml . -RUN mvn dependency:go-offline -B - -# Copy source code -COPY src ./src - -# Build the application -RUN mvn clean package -DskipTests - -# Use OpenJDK 8 runtime for the final image -FROM eclipse-temurin:8-jre - -WORKDIR /app - -# Copy the built jar file -COPY --from=build /app/target/photo-album-*.jar app.jar - -# Expose port -EXPOSE 8080 - -# Set JVM options for container environment -ENV JAVA_OPTS="-Xmx512m -Xms256m" - -# Run the application -ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 1e4200d..b10a897 100644 --- a/README.md +++ b/README.md @@ -1,245 +1,64 @@ -# Photo Album Application - Java Spring Boot with Oracle DB +# PhotoAlbum - Servlet/JSP Photo Gallery -A photo gallery application built with Spring Boot and Oracle Database, featuring drag-and-drop upload, responsive gallery view, and full-size photo details with navigation. - -## Features - -- šŸ“¤ **Photo Upload**: Drag-and-drop or click to upload multiple photos -- šŸ–¼ļø **Gallery View**: Responsive grid layout for browsing uploaded photos -- šŸ” **Photo Detail View**: Click any photo to view full-size with metadata and navigation -- šŸ“Š **Metadata Display**: View file size, dimensions, aspect ratio, and upload timestamp -- ā¬…ļøāž”ļø **Photo Navigation**: Previous/Next buttons to browse through photos -- āœ… **Validation**: File type and size validation (JPEG, PNG, GIF, WebP; max 10MB) -- šŸ—„ļø **Database Storage**: Photo data stored as BLOBs in Oracle Database -- šŸ—‘ļø **Delete Photos**: Remove photos from both gallery and detail views -- šŸŽØ **Modern UI**: Clean, responsive design with Bootstrap 5 - -## Technology Stack - -- **Framework**: Spring Boot 2.7.18 (Java 8) -- **Database**: Oracle Database 21c Express Edition -- **Templating**: Thymeleaf -- **Build Tool**: Maven -- **Frontend**: Bootstrap 5.3.0, Vanilla JavaScript -- **Containerization**: Docker & Docker Compose +A simple photo gallery web application built with pure Servlet/JSP and H2 in-memory database. ## Prerequisites -- Docker Desktop installed and running -- Docker Compose (included with Docker Desktop) -- Minimum 4GB RAM available for Oracle DB container +- Java 8+ +- Apache Ant 1.9+ +- Application Server: Tomcat 8.5+ / GlassFish 7 / WebSphere Liberty 25.x ## Quick Start -1. **Clone the repository**: - ```bash - git clone https://github.com/Azure-Samples/PhotoAlbum-Java.git - cd PhotoAlbum-Java - ``` - -2. **Start the application**: - ```bash - # Use docker-compose directly - docker-compose up --build -d - ``` - - This will: - - Start Oracle Database 21c Express Edition container - - Build the Java Spring Boot application - - Start the Photo Album application container - - Automatically create the database schema using JPA/Hibernate - -3. **Wait for services to start**: - - Oracle DB takes 2-3 minutes to initialize on first run - - Application will start once Oracle is healthy - -4. **Access the application**: - - Open your browser and navigate to: **http://localhost:8080** - - The application should be running and ready to use - -## Services - -## Oracle Database -- **Image**: `container-registry.oracle.com/database/express:21.3.0-xe` -- **Ports**: - - `1521` (database) - mapped to host port 1521 - - `5500` (Enterprise Manager) - mapped to host port 5500 -- **Database**: `XE` (Express Edition) -- **Schema**: `photoalbum` -- **Username/Password**: `photoalbum/photoalbum` - -## Photo Album Java Application -- **Port**: `8080` (mapped to host port 8080) -- **Framework**: Spring Boot 2.7.18 -- **Java Version**: 8 -- **Database**: Connects to Oracle container -- **Photo Storage**: All photos stored as BLOBs in database (no file system storage) -- **UUID System**: Each photo gets a globally unique identifier for cache-busting - -## Database Setup - -The application uses Spring Data JPA with Hibernate for automatic schema management: - -1. **Automatic Schema Creation**: Hibernate automatically creates tables and indexes -2. **User Creation**: Oracle init scripts create the `photoalbum` user -3. **No Manual Setup Required**: Everything is handled automatically - -### Database Schema - -The application creates the following table structure in Oracle: - -#### PHOTOS Table -- `ID` (VARCHAR2(36), Primary Key, UUID Generated) -- `ORIGINAL_FILE_NAME` (VARCHAR2(255), Not Null) -- `STORED_FILE_NAME` (VARCHAR2(255), Not Null) -- `FILE_PATH` (VARCHAR2(500), Nullable) -- `FILE_SIZE` (NUMBER, Not Null) -- `MIME_TYPE` (VARCHAR2(50), Not Null) -- `UPLOADED_AT` (TIMESTAMP, Not Null, Default SYSTIMESTAMP) -- `WIDTH` (NUMBER, Nullable) -- `HEIGHT` (NUMBER, Nullable) -- `PHOTO_DATA` (BLOB, Not Null) - -#### Indexes -- `IDX_PHOTOS_UPLOADED_AT` (Index on UPLOADED_AT for chronological queries) - -#### UUID Generation -- **Java**: `UUID.randomUUID().toString()` generates unique identifiers -- **Benefits**: Eliminates browser caching issues, globally unique across databases -- **Format**: Standard UUID format (36 characters with hyphens) - -## Storage Architecture - -### Database BLOB Storage (Current Implementation) -- **Photos**: Stored as BLOB data directly in the database -- **Benefits**: - - No file system dependencies - - ACID compliance for photo operations - - Simplified backup and migration - - Perfect for containerized deployments -- **Trade-offs**: Database size increases, but suitable for moderate photo volumes - -## Development - -### Running Locally (without Docker) - -1. **Install Oracle Database** (or use Oracle XE) -2. **Create database user**: - ```sql - CREATE USER photoalbum IDENTIFIED BY photoalbum; - GRANT CONNECT, RESOURCE, DBA TO photoalbum; - ``` -3. **Update application.properties**: - ```properties - spring.datasource.url=jdbc:oracle:thin:@localhost:1521:XE - spring.datasource.username=photoalbum - spring.datasource.password=photoalbum - spring.jpa.hibernate.ddl-auto=create - ``` -4. **Run the application**: - ```bash - mvn spring-boot:run - ``` - -### Building from Source +### 1. Build ```bash -# Build the JAR file -mvn clean package - -# Run the JAR file -java -jar target/photo-album-1.0.0.jar +ant war ``` -## Troubleshooting - -### Oracle Database Issues - -1. **Oracle container won't start**: - ```bash - # Check container logs - docker-compose logs oracle-db - - # Increase Docker memory allocation to at least 4GB - ``` - -2. **Database connection errors**: - ```bash - # Verify Oracle is ready - docker exec -it photoalbum-oracle sqlplus photoalbum/photoalbum@//localhost:1521/XE - ``` - -3. **Permission errors**: - ```bash - # Check Oracle init scripts ran - docker-compose logs oracle-db | grep "setup" - ``` - -### Application Issues - -1. **View application logs**: - ```bash - docker-compose logs photoalbum-java-app - ``` - -2. **Rebuild application**: - ```bash - docker-compose up --build - ``` - -3. **Reset database (nuclear option)**: - ```bash - docker-compose down -v - docker-compose up --build - ``` - -## Stopping the Application +### 2. Deploy (choose one) +**GlassFish:** ```bash -# Stop services -docker-compose down - -# Stop and remove all data (including database) -docker-compose down -v +cp dist/photo-album.war $GLASSFISH_HOME/glassfish/domains/domain1/autodeploy/ +$GLASSFISH_HOME/bin/asadmin start-domain +# Access: http://localhost:8080/photo-album/ ``` -## Enterprise Manager (Optional) - -Oracle Enterprise Manager is available at `http://localhost:5500/em` for database administration: -- **Username**: `system` -- **Password**: `photoalbum` -- **Container**: `XE` - -## Performance Notes +**WebSphere Liberty:** +```bash +cp dist/photo-album.war $WLP_HOME/usr/servers/defaultServer/dropins/ +$WLP_HOME/bin/server run defaultServer +# Access: http://localhost:9080/photo-album/ +``` -- Oracle XE has limitations (max 2 CPU threads, 2GB RAM, 12GB storage) -- BLOB storage in database impacts performance at scale -- Suitable for development and small-scale deployments +**Tomcat:** +```bash +cp dist/photo-album.war $CATALINA_HOME/webapps/ +$CATALINA_HOME/bin/catalina.sh run +# Access: http://localhost:8080/photo-album/ +``` -## Project Structure +### 3. Stop Server -``` -PhotoAlbum/ -ā”œā”€ā”€ src/ # Java source code -ā”œā”€ā”€ oracle-init/ # Oracle initialization scripts -ā”œā”€ā”€ docker-compose.yml # Oracle + Application services -ā”œā”€ā”€ Dockerfile # Application container build -ā”œā”€ā”€ pom.xml # Maven dependencies and build config -└── README.md # Project documentation +**GlassFish:** +```bash +$GLASSFISH_HOME/bin/asadmin stop-domain domain1 ``` -## Contributing - -When contributing to this project: +**WebSphere Liberty:** +```bash +$WLP_HOME/bin/server stop defaultServer +``` -- Follow Spring Boot best practices -- Maintain database compatibility -- Ensure UI/UX consistency -- Test both local Docker and Azure deployment scenarios -- Update documentation for any architectural changes -- Preserve UUID system integrity -- Add appropriate tests for new features +**Tomcat:** +```bash +$CATALINA_HOME/bin/catalina.sh stop +``` -## License +## Tech Stack -This project is provided as-is for educational and demonstration purposes. \ No newline at end of file +- Jakarta Servlet 6.0 / JSP 3.1 / JSTL 3.0 +- Pure JDBC (no ORM) +- H2 2.2.224 in-memory database (auto-initialized) +- Apache Ant 1.9+ build diff --git a/azure-reset.ps1 b/azure-reset.ps1 deleted file mode 100644 index 8a382ed..0000000 --- a/azure-reset.ps1 +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env pwsh - -# Quick Azure Photo Album Cleanup Script -# This script provides a streamlined cleanup for photo-album resources - -param( - [Parameter(Mandatory=$true, HelpMessage="The name of the Azure resource group")] - [string]$ResourceGroupName, - - [Parameter(HelpMessage="Skip confirmation prompts and clean everything")] - [switch]$Force, - - [Parameter(HelpMessage="Clean only ACR images")] - [switch]$AcrOnly, - - [Parameter(HelpMessage="Clean only AKS namespace")] - [switch]$AksOnly -) - -function Write-ColoredOutput { - param([string]$Message, [string]$Color = "White") - - $colorCode = switch ($Color) { - "Red" { "`e[31m" } - "Green" { "`e[32m" } - "Yellow" { "`e[33m" } - "Blue" { "`e[36m" } - default { "`e[0m" } - } - Write-Host "${colorCode}${Message}`e[0m" -} - -function Confirm-Action { - param([string]$Message) - - if ($Force) { return $true } - - Write-ColoredOutput $Message "Yellow" - $response = Read-Host "Continue? (y/N)" - return ($response -eq "y" -or $response -eq "yes") -} - -Write-ColoredOutput "=== Quick Azure Cleanup for PhotoAlbum ===" "Green" -Write-ColoredOutput "Resource Group: $ResourceGroupName" "Blue" - -# Validate Azure CLI -try { - az account show --query "id" -o tsv | Out-Null - if ($LASTEXITCODE -ne 0) { throw } - Write-ColoredOutput "āœ“ Azure CLI authenticated" "Green" -} catch { - Write-ColoredOutput "āœ— Please login with 'az login'" "Red" - exit 1 -} - -# Verify resource group -if ((az group exists --name $ResourceGroupName) -eq "false") { - Write-ColoredOutput "āœ— Resource group '$ResourceGroupName' not found" "Red" - exit 1 -} - -# Clean ACR Images -if (-not $AksOnly) { - Write-ColoredOutput "`n=== ACR Cleanup ===" "Blue" - - $acrs = az acr list --resource-group $ResourceGroupName --query "[].name" -o json | ConvertFrom-Json - - if ($acrs.Count -eq 0) { - Write-ColoredOutput "No ACR found in resource group" "Yellow" - } else { - foreach ($acrName in $acrs) { - Write-ColoredOutput "Processing ACR: $acrName" "Yellow" - - $repos = az acr repository list --name $acrName -o json | ConvertFrom-Json - - if ($repos.Count -eq 0) { - Write-ColoredOutput " No repositories found" "Yellow" - continue - } - - if (Confirm-Action "Delete all images in ACR '$acrName' ($($repos.Count) repositories)?") { - foreach ($repo in $repos) { - Write-ColoredOutput " Cleaning repository: $repo" "Yellow" - az acr repository delete --name $acrName --repository $repo --yes 2>$null - if ($LASTEXITCODE -eq 0) { - Write-ColoredOutput " āœ“ Deleted: $repo" "Green" - } else { - Write-ColoredOutput " āœ— Failed to delete: $repo" "Red" - } - } - } else { - Write-ColoredOutput " Skipped ACR cleanup" "Yellow" - } - } - } -} - -# Clean AKS Namespace -if (-not $AcrOnly) { - Write-ColoredOutput "`n=== AKS Namespace Cleanup ===" "Blue" - - $aksClusters = az aks list --resource-group $ResourceGroupName --query "[].name" -o json | ConvertFrom-Json - - if ($aksClusters.Count -eq 0) { - Write-ColoredOutput "No AKS clusters found in resource group" "Yellow" - } else { - foreach ($aksName in $aksClusters) { - Write-ColoredOutput "Processing AKS: $aksName" "Yellow" - - # Get credentials - az aks get-credentials --resource-group $ResourceGroupName --name $aksName --overwrite-existing 2>$null - if ($LASTEXITCODE -ne 0) { - Write-ColoredOutput " āœ— Failed to get AKS credentials" "Red" - continue - } - - # Check kubectl - kubectl version --client=true 2>$null | Out-Null - if ($LASTEXITCODE -ne 0) { - Write-ColoredOutput " āœ— kubectl not available" "Red" - continue - } - - # Check namespace - kubectl get namespace photo-album 2>$null | Out-Null - if ($LASTEXITCODE -ne 0) { - Write-ColoredOutput " photo-album namespace not found" "Yellow" - continue - } - - if (Confirm-Action "Delete photo-album namespace in AKS '$aksName'?") { - kubectl delete namespace photo-album --timeout=60s - if ($LASTEXITCODE -eq 0) { - Write-ColoredOutput " āœ“ Namespace deleted" "Green" - } else { - Write-ColoredOutput " āœ— Failed to delete namespace" "Red" - } - } else { - Write-ColoredOutput " Skipped namespace cleanup" "Yellow" - } - } - } -} - -Write-ColoredOutput "`nāœ“ Cleanup completed!" "Green" \ No newline at end of file diff --git a/azure-setup.ps1 b/azure-setup.ps1 deleted file mode 100644 index b83c4de..0000000 --- a/azure-setup.ps1 +++ /dev/null @@ -1,306 +0,0 @@ -#!/usr/bin/env pwsh - -# Colors for output using ANSI escape codes (works in modern PowerShell) -$RED = "`e[31m" -$GREEN = "`e[32m" -$YELLOW = "`e[33m" -$NC = "`e[0m" # No Color - -Write-Host "${GREEN}=== Azure Photo Album Resources Setup ===${NC}" -NoNewline -Write-Host "" - -# Variables -$RANDOM_SUFFIX = -join ((1..3) | ForEach-Object { '{0:x}' -f (Get-Random -Maximum 256) }) -$RESOURCE_GROUP = "photo-album-resources-${RANDOM_SUFFIX}" -$LOCATION = "westus3" -$ACR_NAME = "photoalbumacr$(Get-Random -Maximum 99999)" -$AKS_NODE_VM_SIZE = "Standard_D8ds_v5" -$POSTGRES_SERVER_NAME = "$RESOURCE_GROUP-postgresql" -$PostgreSQL_SKU = "Standard_D4ads_v5" -$POSTGRES_ADMIN_USER="photoalbum_admin" -$POSTGRES_ADMIN_PASSWORD="P@ssw0rd123!" -$POSTGRES_DATABASE_NAME="photoalbum" -$POSTGRES_APP_USER="photoalbum" -$POSTGRES_APP_PASSWORD="photoalbum" - -Write-Host "${YELLOW}Using default subscription...${NC}" -NoNewline -Write-Host "" -az account show --query "{Name:name, SubscriptionId:id}" -o table - -if ($LASTEXITCODE -ne 0) { - Write-Host "${RED}Failed to get Azure account information. Please ensure you are logged in with 'az login'${NC}" -NoNewline - Write-Host "" - exit 1 -} - -# Create Resource Group -Write-Host "${YELLOW}Creating resource group: $RESOURCE_GROUP${NC}" -NoNewline -Write-Host "" -az group create ` - --name $RESOURCE_GROUP ` - --location $LOCATION - -if ($LASTEXITCODE -ne 0) { - Write-Host "${RED}Failed to create resource group${NC}" -NoNewline - Write-Host "" - exit 1 -} - -# Create Azure Container Registry -Write-Host "${YELLOW}Creating Azure Container Registry: $ACR_NAME${NC}" -NoNewline -Write-Host "" -az acr create ` - --name $ACR_NAME ` - --resource-group $RESOURCE_GROUP ` - --location $LOCATION ` - --sku Basic ` - --admin-enabled true - -if ($LASTEXITCODE -ne 0) { - Write-Host "${RED}Failed to create Azure Container Registry${NC}" -NoNewline - Write-Host "" - exit 1 -} - -# Create Azure Kubernetes Service (AKS) Cluster -Write-Host "${YELLOW}Creating AKS Cluster: ${RESOURCE_GROUP}-aks${NC}" -NoNewline -Write-Host "" -az aks create ` - --resource-group $RESOURCE_GROUP ` - --name "$RESOURCE_GROUP-aks" ` - --node-count 2 ` - --generate-ssh-keys ` - --location $LOCATION ` - --node-vm-size $AKS_NODE_VM_SIZE - -if ($LASTEXITCODE -ne 0) { - Write-Host "${RED}Failed to create AKS cluster${NC}" -NoNewline - Write-Host "" - exit 1 -} - -# Grant AKS permission to pull images from ACR -Write-Host "${YELLOW}Granting AKS permission to pull images from ACR: $ACR_NAME${NC}" -NoNewline -Write-Host "" -az aks update ` - --resource-group $RESOURCE_GROUP ` - --name "$RESOURCE_GROUP-aks" ` - --attach-acr $ACR_NAME - -if ($LASTEXITCODE -ne 0) { - Write-Host "${RED}Failed to attach ACR to AKS cluster${NC}" -NoNewline - Write-Host "" - exit 1 -} - -# Create PostgreSQL Flexible Server -Write-Host "${YELLOW}Creating PostgreSQL Flexible Server: ${POSTGRES_SERVER_NAME}${NC}" -NoNewline -Write-Host "" - -az postgres flexible-server create ` - --resource-group "$RESOURCE_GROUP" ` - --name "$POSTGRES_SERVER_NAME" ` - --location "$LOCATION" ` - --admin-user "$POSTGRES_ADMIN_USER" ` - --admin-password "$POSTGRES_ADMIN_PASSWORD" ` - --version "15" ` - --sku-name $PostgreSQL_SKU ` - --storage-size "32" ` - --backup-retention "7" ` - --public-access "0.0.0.0" ` - -if ($LASTEXITCODE -ne 0) { - Write-Host "${RED}Failed to create PostgreSQL Flexible Server${NC}" -NoNewline - Write-Host "" - exit 1 -} - -# Create application database -Write-Host "${YELLOW}Creating database: ${POSTGRES_DATABASE_NAME}${NC}" -NoNewline -Write-Host "" - -az postgres flexible-server db create ` - --resource-group "$RESOURCE_GROUP" ` - --server-name "$POSTGRES_SERVER_NAME" ` - --database-name "$POSTGRES_DATABASE_NAME" ` - -if ($LASTEXITCODE -ne 0) { - Write-Host "${RED}Failed to create PostgreSQL database${NC}" -NoNewline - Write-Host "" - exit 1 -} - -# Configure firewall for Azure services -Write-Host "${YELLOW}Configuring firewall rules...${NC}" -NoNewline -Write-Host "" -az postgres flexible-server firewall-rule create ` - --resource-group "$RESOURCE_GROUP" ` - --name "$POSTGRES_SERVER_NAME" ` - --rule-name "AllowAzureServices" ` - --start-ip-address "0.0.0.0" ` - --end-ip-address "0.0.0.0" ` - -if ($LASTEXITCODE -ne 0) { - Write-Host "${RED}Failed to configure firewall rules${NC}" -NoNewline - Write-Host "" - exit 1 -} - -# Add current IP to firewall -$CURRENT_IP = (Invoke-RestMethod -Uri "https://api.ipify.org" -UseBasicParsing).Trim() -if ($CURRENT_IP) { - Write-Host "${YELLOW}Adding your current IP ($CURRENT_IP) to firewall...${NC}" -NoNewline - Write-Host "" - az postgres flexible-server firewall-rule create ` - --resource-group "$RESOURCE_GROUP" ` - --name "$POSTGRES_SERVER_NAME" ` - --rule-name "AllowCurrentIP" ` - --start-ip-address "$CURRENT_IP" ` - --end-ip-address "$CURRENT_IP" ` - - if ($LASTEXITCODE -ne 0) { - Write-Host "${RED}Failed to add your current IP to firewall${NC}" -NoNewline - Write-Host "" - exit 1 -} -} - -# Get server FQDN -Write-Host "${YELLOW}Getting server connection details...${NC}" -NoNewline -Write-Host "" -$SERVER_FQDN = az postgres flexible-server show ` - --resource-group "$RESOURCE_GROUP" ` - --name "$POSTGRES_SERVER_NAME" ` - --query "fullyQualifiedDomainName" ` - --output tsv - -# Wait a moment for server to be fully ready -Write-Host "${YELLOW}Waiting for server to be fully ready...${NC}" -NoNewline -Write-Host "" -Start-Sleep -Seconds 30 - -# Setup application user and tables -Write-Host "${YELLOW}Setting up database user and tables...${NC}" -NoNewline -Write-Host "" - -# Create application user using the more reliable execute command -Write-Host "${YELLOW}Creating application user...${NC}" -NoNewline -Write-Host "" -try { - az postgres flexible-server execute ` - --name "$POSTGRES_SERVER_NAME" ` - --admin-user "$POSTGRES_ADMIN_USER" ` - --admin-password "$POSTGRES_ADMIN_PASSWORD" ` - --database-name "postgres" ` - --querytext "CREATE USER photoalbum WITH PASSWORD 'photoalbum';" -} catch { - Write-Host "${YELLOW}User may already exist, continuing...${NC}" -NoNewline - Write-Host "" -} - -# Grant database connection privileges -Write-Host "${YELLOW}Granting database connection privileges...${NC}" -NoNewline -Write-Host "" -try { - az postgres flexible-server execute ` - --name "$POSTGRES_SERVER_NAME" ` - --admin-user "$POSTGRES_ADMIN_USER" ` - --admin-password "$POSTGRES_ADMIN_PASSWORD" ` - --database-name "postgres" ` - --querytext "GRANT CONNECT ON DATABASE photoalbum TO photoalbum;" -} catch { - Write-Host "${YELLOW}Grant may have failed, continuing...${NC}" -NoNewline - Write-Host "" -} - -# Grant schema privileges on the photoalbum database -Write-Host "${YELLOW}Granting schema privileges...${NC}" -NoNewline -Write-Host "" -try { - az postgres flexible-server execute ` - --name "$POSTGRES_SERVER_NAME" ` - --admin-user "$POSTGRES_ADMIN_USER" ` - --admin-password "$POSTGRES_ADMIN_PASSWORD" ` - --database-name "photoalbum" ` - --querytext "GRANT ALL PRIVILEGES ON SCHEMA public TO photoalbum;" -} catch { - Write-Host "${YELLOW}Schema privileges may have failed, continuing...${NC}" -NoNewline - Write-Host "" -} - -# Grant privileges on future objects (so Hibernate can create and manage tables) -Write-Host "${YELLOW}Setting up future object privileges for Hibernate...${NC}" -NoNewline -Write-Host "" -try { - az postgres flexible-server execute ` - --name "$POSTGRES_SERVER_NAME" ` - --admin-user "$POSTGRES_ADMIN_USER" ` - --admin-password "$POSTGRES_ADMIN_PASSWORD" ` - --database-name "photoalbum" ` - --querytext "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO photoalbum;" -} catch { - Write-Host "${YELLOW}Default privileges may have failed, continuing...${NC}" -NoNewline - Write-Host "" -} - -Write-Host "${GREEN}Database user and schema setup completed! Hibernate will create and manage tables.${NC}" -NoNewline -Write-Host "" - -# Store the datasource URL for later use -$DATASOURCE_URL = "jdbc:postgresql://${SERVER_FQDN}:5432/$POSTGRES_DATABASE_NAME" -Write-Host "${YELLOW}Datasource URL: $DATASOURCE_URL${NC}" -NoNewline -Write-Host "" - -# Store PostgreSQL credentials in environment variables -Write-Host "${YELLOW}Storing PostgreSQL credentials in environment variables...${NC}" -NoNewline -Write-Host "" -$env:POSTGRES_SERVER = "${POSTGRES_SERVER_NAME}.postgres.database.azure.com" -$env:POSTGRES_USER = $POSTGRES_APP_USER -$env:POSTGRES_PASSWORD = $POSTGRES_APP_PASSWORD -$env:POSTGRES_CONNECTION_STRING = $DATASOURCE_URL - -# Write environment variables to .env file -Write-Host "${YELLOW}Writing environment variables to .env file...${NC}" -NoNewline -Write-Host "" -$SCRIPT_ROOT = Split-Path -Parent $MyInvocation.MyCommand.Path -$ENV_FILE = Join-Path $SCRIPT_ROOT ".env" - -$ENV_CONTENT = @" -# Azure PostgreSQL Configuration -POSTGRES_SERVER=$env:POSTGRES_SERVER -POSTGRES_USER=$env:POSTGRES_USER -POSTGRES_PASSWORD=$env:POSTGRES_PASSWORD -POSTGRES_CONNECTION_STRING=$env:POSTGRES_CONNECTION_STRING - -# Azure Resources -RESOURCE_GROUP=$RESOURCE_GROUP -ACR_NAME=$ACR_NAME -AKS_CLUSTER_NAME=$RESOURCE_GROUP-aks -LOCATION=$LOCATION -"@ - -$ENV_CONTENT | Out-File -FilePath $ENV_FILE -Encoding UTF8 -Write-Host "${GREEN}Environment variables written to: $ENV_FILE${NC}" -NoNewline -Write-Host "" - -Write-Host "${GREEN}=== Setup Complete ===${NC}" -NoNewline -Write-Host "" - -# Output important information -Write-Host "${GREEN}Resources created successfully:${NC}" -NoNewline -Write-Host "" -Write-Host "Resource Group: $RESOURCE_GROUP" -Write-Host "Container Registry: $ACR_NAME" -Write-Host "AKS Cluster: $RESOURCE_GROUP-aks" -Write-Host "PostgreSQL Server: $POSTGRES_SERVER_NAME" -Write-Host "Location: $LOCATION" -Write-Host "" -Write-Host "${GREEN}PostgreSQL Connection Details (stored in environment variables and .env file):${NC}" -NoNewline -Write-Host "" -Write-Host "POSTGRES_SERVER: $env:POSTGRES_SERVER" -Write-Host "POSTGRES_USER: $env:POSTGRES_USER" -Write-Host "POSTGRES_PASSWORD: $env:POSTGRES_PASSWORD" -Write-Host "POSTGRES_CONNECTION_STRING: $env:POSTGRES_CONNECTION_STRING" -Write-Host "" -Write-Host "${GREEN}All configuration has been saved to .env file in the project root.${NC}" -NoNewline -Write-Host "" diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..7829101 --- /dev/null +++ b/build.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 2d74e35..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,49 +0,0 @@ -services: - # Oracle Database service (Oracle Database Free 23ai - supports ARM64 and x86_64) - oracle-db: - image: gvenzl/oracle-free:latest - container_name: photoalbum-oracle - environment: - - ORACLE_PASSWORD=photoalbum - - APP_USER=photoalbum - - APP_USER_PASSWORD=photoalbum - ports: - - "1521:1521" - volumes: - - oracle_data:/opt/oracle/oradata - - ./oracle-init:/container-entrypoint-initdb.d - networks: - - photoalbum-network - healthcheck: - test: ["CMD-SHELL", "healthcheck.sh"] - interval: 30s - timeout: 10s - retries: 15 - start_period: 180s - - # Photo Album Java Application - photoalbum-java-app: - build: - context: . - dockerfile: Dockerfile - container_name: photoalbum-java-app - environment: - - SPRING_PROFILES_ACTIVE=docker - - SPRING_DATASOURCE_URL=jdbc:oracle:thin:@oracle-db:1521/FREEPDB1 - - SPRING_DATASOURCE_USERNAME=photoalbum - - SPRING_DATASOURCE_PASSWORD=photoalbum - ports: - - "8080:8080" - depends_on: - oracle-db: - condition: service_healthy - networks: - - photoalbum-network - restart: on-failure - -volumes: - oracle_data: - -networks: - photoalbum-network: - driver: bridge diff --git a/lib/commons-fileupload-1.5.jar b/lib/commons-fileupload-1.5.jar new file mode 100644 index 0000000..5e60875 Binary files /dev/null and b/lib/commons-fileupload-1.5.jar differ diff --git a/lib/commons-io-2.11.0.jar b/lib/commons-io-2.11.0.jar new file mode 100644 index 0000000..be507d9 Binary files /dev/null and b/lib/commons-io-2.11.0.jar differ diff --git a/lib/h2-2.2.224.jar b/lib/h2-2.2.224.jar new file mode 100644 index 0000000..ff1997a Binary files /dev/null and b/lib/h2-2.2.224.jar differ diff --git a/lib/jakarta.servlet-api-6.0.0.jar b/lib/jakarta.servlet-api-6.0.0.jar new file mode 100644 index 0000000..d15c10c Binary files /dev/null and b/lib/jakarta.servlet-api-6.0.0.jar differ diff --git a/oracle-init/01-create-user.sql b/oracle-init/01-create-user.sql deleted file mode 100644 index 32ad600..0000000 --- a/oracle-init/01-create-user.sql +++ /dev/null @@ -1,36 +0,0 @@ --- This script runs automatically when Oracle XE container starts --- It creates the photoalbum user and grants necessary privileges - -ALTER SESSION SET "_ORACLE_SCRIPT"=true; - --- Create photoalbum user -CREATE USER photoalbum IDENTIFIED BY photoalbum; - --- Grant system privileges -GRANT CONNECT TO photoalbum; -GRANT RESOURCE TO photoalbum; -GRANT DBA TO photoalbum; -GRANT CREATE SESSION TO photoalbum; -GRANT CREATE TABLE TO photoalbum; -GRANT CREATE SEQUENCE TO photoalbum; -GRANT CREATE VIEW TO photoalbum; -GRANT CREATE PROCEDURE TO photoalbum; -GRANT CREATE TRIGGER TO photoalbum; -GRANT CREATE TYPE TO photoalbum; -GRANT CREATE SYNONYM TO photoalbum; -GRANT UNLIMITED TABLESPACE TO photoalbum; - --- Grant object privileges needed by Hibernate -GRANT SELECT ANY DICTIONARY TO photoalbum; -GRANT CREATE ANY INDEX TO photoalbum; -GRANT ALTER ANY INDEX TO photoalbum; -GRANT DROP ANY INDEX TO photoalbum; - --- Set default and temporary tablespace -ALTER USER photoalbum DEFAULT TABLESPACE USERS; -ALTER USER photoalbum TEMPORARY TABLESPACE TEMP; - --- Commit the changes -COMMIT; - -EXIT; \ No newline at end of file diff --git a/oracle-init/02-verify-user.sql b/oracle-init/02-verify-user.sql deleted file mode 100644 index 91c82a1..0000000 --- a/oracle-init/02-verify-user.sql +++ /dev/null @@ -1,14 +0,0 @@ --- Verification script to check if photoalbum user exists -ALTER SESSION SET "_ORACLE_SCRIPT"=true; - --- Check if user exists -SELECT username, account_status, default_tablespace -FROM dba_users -WHERE username = 'PHOTOALBUM'; - --- Show granted privileges -SELECT grantee, privilege -FROM dba_sys_privs -WHERE grantee = 'PHOTOALBUM'; - -EXIT; \ No newline at end of file diff --git a/oracle-init/create-user.sh b/oracle-init/create-user.sh deleted file mode 100644 index c15ee5f..0000000 --- a/oracle-init/create-user.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -# This script ensures the photoalbum user is created in Oracle XE - -# Wait for Oracle to be fully ready -echo "Waiting for Oracle to be ready..." -sleep 30 - -# Connect to Oracle as SYSTEM and create the photoalbum user -sqlplus -s system/photoalbum@//localhost:1521/XE < - - 4.0.0 - - - org.springframework.boot - spring-boot-starter-parent - 2.7.18 - - - - com.photoalbum - photo-album - 1.0.0 - jar - - Photo Album - A simple photo storage and gallery application built with Spring Boot and Oracle DB - - - 1.8 - 8 - 8 - UTF-8 - - - - - - org.springframework.boot - spring-boot-starter-web - - - - - org.springframework.boot - spring-boot-starter-thymeleaf - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - - - com.oracle.database.jdbc - ojdbc8 - runtime - - - - - org.springframework.boot - spring-boot-starter-validation - - - - - commons-io - commons-io - 2.11.0 - - - - - org.springframework.boot - spring-boot-starter-json - - - - - org.springframework.boot - spring-boot-starter-test - test - - - - - com.h2database - h2 - test - - - - - org.springframework.boot - spring-boot-devtools - true - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - \ No newline at end of file diff --git a/resources/application.properties b/resources/application.properties new file mode 100644 index 0000000..178888f --- /dev/null +++ b/resources/application.properties @@ -0,0 +1,5 @@ +# H2 In-Memory Database +app.datasource.url=jdbc:h2:mem:photodb;DB_CLOSE_DELAY=-1;MODE=Oracle +app.datasource.username=sa +app.datasource.password= +app.datasource.driver-class-name=org.h2.Driver diff --git a/src/com/photoalbum/dao/PhotoDAO.java b/src/com/photoalbum/dao/PhotoDAO.java new file mode 100644 index 0000000..9732ab3 --- /dev/null +++ b/src/com/photoalbum/dao/PhotoDAO.java @@ -0,0 +1,267 @@ +package com.photoalbum.dao; + +import com.photoalbum.model.Photo; +import com.photoalbum.util.DatabaseConfig; + +import java.sql.*; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Data Access Object for Photo entity using pure JDBC + * Handles all database operations for photos stored in Oracle database + */ +public class PhotoDAO { + + /** + * Get database connection from configuration + */ + private Connection getConnection() throws SQLException { + return DatabaseConfig.getConnection(); + } + + /** + * Save a new photo to the database + */ + public Photo save(Photo photo) throws SQLException { + String sql = "INSERT INTO photos (id, original_file_name, photo_data, stored_file_name, " + + "file_path, file_size, mime_type, uploaded_at, width, height) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + try (Connection conn = getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + // Generate ID if not set + if (photo.getId() == null || photo.getId().isEmpty()) { + photo.setId(UUID.randomUUID().toString()); + } + + // Set uploaded time if not set + if (photo.getUploadedAt() == null) { + photo.setUploadedAt(LocalDateTime.now()); + } + + stmt.setString(1, photo.getId()); + stmt.setString(2, photo.getOriginalFileName()); + stmt.setBytes(3, photo.getPhotoData()); + stmt.setString(4, photo.getStoredFileName()); + stmt.setString(5, photo.getFilePath()); + stmt.setLong(6, photo.getFileSize()); + stmt.setString(7, photo.getMimeType()); + stmt.setTimestamp(8, Timestamp.valueOf(photo.getUploadedAt())); + + // Set width and height (can be null) + if (photo.getWidth() != null) { + stmt.setInt(9, photo.getWidth()); + } else { + stmt.setNull(9, Types.INTEGER); + } + + if (photo.getHeight() != null) { + stmt.setInt(10, photo.getHeight()); + } else { + stmt.setNull(10, Types.INTEGER); + } + + stmt.executeUpdate(); + return photo; + } + } + + /** + * Find a photo by ID + */ + public Photo findById(String id) throws SQLException { + String sql = "SELECT id, original_file_name, photo_data, stored_file_name, file_path, " + + "file_size, mime_type, uploaded_at, width, height FROM photos WHERE id = ?"; + + try (Connection conn = getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setString(1, id); + + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return mapResultSetToPhoto(rs); + } + } + } + + return null; + } + + /** + * Find all photos, ordered by upload date (newest first) + */ + public List findAll() throws SQLException { + String sql = "SELECT id, original_file_name, photo_data, stored_file_name, file_path, " + + "file_size, mime_type, uploaded_at, width, height FROM photos " + + "ORDER BY uploaded_at DESC"; + + List photos = new ArrayList<>(); + + try (Connection conn = getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + + while (rs.next()) { + photos.add(mapResultSetToPhoto(rs)); + } + } + + return photos; + } + + /** + * Find all photos with pagination + */ + public List findAll(int offset, int limit) throws SQLException { + String sql = "SELECT id, original_file_name, photo_data, stored_file_name, file_path, " + + "file_size, mime_type, uploaded_at, width, height FROM photos " + + "ORDER BY uploaded_at DESC " + + "OFFSET ? ROWS FETCH NEXT ? ROWS ONLY"; + + List photos = new ArrayList<>(); + + try (Connection conn = getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setInt(1, offset); + stmt.setInt(2, limit); + + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + photos.add(mapResultSetToPhoto(rs)); + } + } + } + + return photos; + } + + /** + * Count total number of photos + */ + public long count() throws SQLException { + String sql = "SELECT COUNT(*) FROM photos"; + + try (Connection conn = getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + + if (rs.next()) { + return rs.getLong(1); + } + } + + return 0; + } + + /** + * Delete a photo by ID + */ + public boolean deleteById(String id) throws SQLException { + String sql = "DELETE FROM photos WHERE id = ?"; + + try (Connection conn = getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setString(1, id); + int rowsAffected = stmt.executeUpdate(); + + return rowsAffected > 0; + } + } + + /** + * Update photo metadata (width, height) + */ + public boolean updateMetadata(String id, Integer width, Integer height) throws SQLException { + String sql = "UPDATE photos SET width = ?, height = ? WHERE id = ?"; + + try (Connection conn = getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + if (width != null) { + stmt.setInt(1, width); + } else { + stmt.setNull(1, Types.INTEGER); + } + + if (height != null) { + stmt.setInt(2, height); + } else { + stmt.setNull(2, Types.INTEGER); + } + + stmt.setString(3, id); + + int rowsAffected = stmt.executeUpdate(); + return rowsAffected > 0; + } + } + + /** + * Check if a photo exists by ID + */ + public boolean exists(String id) throws SQLException { + String sql = "SELECT 1 FROM photos WHERE id = ?"; + + try (Connection conn = getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setString(1, id); + + try (ResultSet rs = stmt.executeQuery()) { + return rs.next(); + } + } + } + + /** + * Helper method to map ResultSet to Photo object + */ + private Photo mapResultSetToPhoto(ResultSet rs) throws SQLException { + Photo photo = new Photo(); + + photo.setId(rs.getString("id")); + photo.setOriginalFileName(rs.getString("original_file_name")); + photo.setPhotoData(rs.getBytes("photo_data")); + photo.setStoredFileName(rs.getString("stored_file_name")); + photo.setFilePath(rs.getString("file_path")); + photo.setFileSize(rs.getLong("file_size")); + photo.setMimeType(rs.getString("mime_type")); + + Timestamp uploadedAt = rs.getTimestamp("uploaded_at"); + if (uploadedAt != null) { + photo.setUploadedAt(uploadedAt.toLocalDateTime()); + } + + int width = rs.getInt("width"); + if (!rs.wasNull()) { + photo.setWidth(width); + } + + int height = rs.getInt("height"); + if (!rs.wasNull()) { + photo.setHeight(height); + } + + return photo; + } + + /** + * Delete all photos (use with caution!) + */ + public int deleteAll() throws SQLException { + String sql = "DELETE FROM photos"; + + try (Connection conn = getConnection(); + Statement stmt = conn.createStatement()) { + + return stmt.executeUpdate(sql); + } + } +} diff --git a/src/main/java/com/photoalbum/model/Photo.java b/src/com/photoalbum/model/Photo.java similarity index 66% rename from src/main/java/com/photoalbum/model/Photo.java rename to src/com/photoalbum/model/Photo.java index d1cea3e..fcaefd5 100644 --- a/src/main/java/com/photoalbum/model/Photo.java +++ b/src/com/photoalbum/model/Photo.java @@ -1,93 +1,22 @@ package com.photoalbum.model; -import javax.persistence.*; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Positive; -import javax.validation.constraints.Size; - import java.time.LocalDateTime; import java.util.UUID; /** * Represents an uploaded photo with metadata for display and management */ -@Entity -@Table(name = "photos", indexes = { - @Index(name = "idx_photos_uploaded_at", columnList = "uploaded_at", unique = false) -}) public class Photo { - /** - * Unique identifier for the photo using UUID - */ - @Id - @Column(name = "id", length = 36) private String id; - - /** - * Original filename as uploaded by user - */ - @NotBlank - @Size(max = 255) - @Column(name = "original_file_name", nullable = false, length = 255) private String originalFileName; - - /** - * Binary photo data stored directly in Oracle database - */ - @Lob - @Column(name = "photo_data", nullable = true) private byte[] photoData; - - /** - * GUID-based filename with extension (for compatibility) - */ - @NotBlank - @Size(max = 255) - @Column(name = "stored_file_name", nullable = false, length = 255) private String storedFileName; - - /** - * Relative path from static resources (for compatibility - not used for DB storage) - */ - @Size(max = 500) - @Column(name = "file_path", length = 500) private String filePath; - - /** - * File size in bytes - */ - @NotNull - @Positive - @Column(name = "file_size", nullable = false, columnDefinition = "NUMBER(19,0)") private Long fileSize; - - /** - * MIME type (e.g., image/jpeg, image/png) - */ - @NotBlank - @Size(max = 50) - @Column(name = "mime_type", nullable = false, length = 50) private String mimeType; - - /** - * Timestamp of upload - */ - @NotNull - @Column(name = "uploaded_at", nullable = false, columnDefinition = "TIMESTAMP DEFAULT SYSTIMESTAMP") private LocalDateTime uploadedAt; - - /** - * Image width in pixels (populated after upload) - */ - @Column(name = "width") private Integer width; - - /** - * Image height in pixels (populated after upload) - */ - @Column(name = "height") private Integer height; // Default constructor diff --git a/src/main/java/com/photoalbum/model/UploadResult.java b/src/com/photoalbum/model/UploadResult.java similarity index 100% rename from src/main/java/com/photoalbum/model/UploadResult.java rename to src/com/photoalbum/model/UploadResult.java diff --git a/src/com/photoalbum/servlet/DetailServlet.java b/src/com/photoalbum/servlet/DetailServlet.java new file mode 100644 index 0000000..53e8576 --- /dev/null +++ b/src/com/photoalbum/servlet/DetailServlet.java @@ -0,0 +1,94 @@ +package com.photoalbum.servlet; + +import com.photoalbum.dao.PhotoDAO; +import com.photoalbum.model.Photo; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.sql.SQLException; + +/** + * Detail Servlet - Displays photo details and handles deletion + * Replaces DetailController from Spring Boot version + */ +public class DetailServlet extends HttpServlet { + + private PhotoDAO photoDAO; + + @Override + public void init() throws ServletException { + super.init(); + photoDAO = new PhotoDAO(); + } + + /** + * GET - Display photo details page + */ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + String photoId = request.getParameter("id"); + + if (photoId == null || photoId.isEmpty()) { + response.sendRedirect(request.getContextPath() + "/"); + return; + } + + try { + // Find photo by ID + Photo photo = photoDAO.findById(photoId); + + if (photo == null) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "Photo not found"); + return; + } + + // Set photo as request attribute + request.setAttribute("photo", photo); + + // Forward to detail.jsp + request.getRequestDispatcher("/WEB-INF/jsp/detail.jsp").forward(request, response); + + } catch (SQLException e) { + log("Error loading photo details", e); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + /** + * POST - Handle photo deletion + */ + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + String action = request.getParameter("action"); + String photoId = request.getParameter("id"); + + if ("delete".equals(action) && photoId != null && !photoId.isEmpty()) { + try { + // Delete photo from database + boolean deleted = photoDAO.deleteById(photoId); + + if (deleted) { + // Redirect to home with success message + response.sendRedirect(request.getContextPath() + "/?success=deleted"); + } else { + // Photo not found + response.sendRedirect(request.getContextPath() + "/?error=notfound"); + } + + } catch (SQLException e) { + log("Error deleting photo", e); + response.sendRedirect(request.getContextPath() + "/?error=database"); + } + } else { + // Invalid request + response.sendRedirect(request.getContextPath() + "/"); + } + } +} diff --git a/src/com/photoalbum/servlet/DownloadServlet.java b/src/com/photoalbum/servlet/DownloadServlet.java new file mode 100644 index 0000000..267bd05 --- /dev/null +++ b/src/com/photoalbum/servlet/DownloadServlet.java @@ -0,0 +1,87 @@ +package com.photoalbum.servlet; + +import com.photoalbum.dao.PhotoDAO; +import com.photoalbum.model.Photo; + +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.sql.SQLException; + +/** + * Download Servlet - Serves photo files as downloads or inline display + * Replaces PhotoFileController from Spring Boot version + */ +public class DownloadServlet extends HttpServlet { + + private PhotoDAO photoDAO; + + @Override + public void init() throws ServletException { + super.init(); + photoDAO = new PhotoDAO(); + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + String photoId = request.getParameter("id"); + String mode = request.getParameter("mode"); // "download" or "inline" + + if (photoId == null || photoId.isEmpty()) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Photo ID is required"); + return; + } + + try { + // Find photo by ID + Photo photo = photoDAO.findById(photoId); + + if (photo == null) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "Photo not found"); + return; + } + + // Get photo data + byte[] photoData = photo.getPhotoData(); + + if (photoData == null || photoData.length == 0) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "Photo data not found"); + return; + } + + // Set response headers + response.setContentType(photo.getMimeType()); + response.setContentLength(photoData.length); + + // Set Content-Disposition header + if ("download".equals(mode)) { + // Force download + response.setHeader("Content-Disposition", + "attachment; filename=\"" + photo.getOriginalFileName() + "\""); + } else { + // Display inline (default) + response.setHeader("Content-Disposition", + "inline; filename=\"" + photo.getOriginalFileName() + "\""); + } + + // Set caching headers + response.setHeader("Cache-Control", "public, max-age=31536000"); + response.setDateHeader("Expires", System.currentTimeMillis() + 31536000000L); + + // Write photo data to response + try (ServletOutputStream out = response.getOutputStream()) { + out.write(photoData); + out.flush(); + } + + } catch (SQLException e) { + log("Error serving photo file", e); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/com/photoalbum/servlet/HomeServlet.java b/src/com/photoalbum/servlet/HomeServlet.java new file mode 100644 index 0000000..0632d9e --- /dev/null +++ b/src/com/photoalbum/servlet/HomeServlet.java @@ -0,0 +1,49 @@ +package com.photoalbum.servlet; + +import com.photoalbum.dao.PhotoDAO; +import com.photoalbum.model.Photo; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.sql.SQLException; +import java.util.List; + +/** + * Home Servlet - Displays the photo gallery + * Replaces HomeController from Spring Boot version + */ +public class HomeServlet extends HttpServlet { + + private PhotoDAO photoDAO; + + @Override + public void init() throws ServletException { + super.init(); + photoDAO = new PhotoDAO(); + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + try { + // Get all photos from database + List photos = photoDAO.findAll(); + + // Set photos as request attribute for JSP + request.setAttribute("photos", photos); + + // Forward to index.jsp + request.getRequestDispatcher("/WEB-INF/jsp/index.jsp").forward(request, response); + + } catch (SQLException e) { + // Log error and show error page + log("Error loading photos", e); + request.setAttribute("errorMessage", "Unable to load photos: " + e.getMessage()); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/com/photoalbum/servlet/UploadServlet.java b/src/com/photoalbum/servlet/UploadServlet.java new file mode 100644 index 0000000..21e9c66 --- /dev/null +++ b/src/com/photoalbum/servlet/UploadServlet.java @@ -0,0 +1,156 @@ +package com.photoalbum.servlet; + +import com.photoalbum.dao.PhotoDAO; +import com.photoalbum.model.Photo; + +import javax.imageio.ImageIO; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.MultipartConfig; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.Part; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.sql.SQLException; +import java.util.UUID; + +/** + * Upload Servlet - Handles photo uploads + * Replaces upload functionality from HomeController + */ +@MultipartConfig( + maxFileSize = 10485760, // 10MB + maxRequestSize = 10485760, // 10MB + fileSizeThreshold = 0 +) +public class UploadServlet extends HttpServlet { + + private PhotoDAO photoDAO; + + @Override + public void init() throws ServletException { + super.init(); + photoDAO = new PhotoDAO(); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + try { + // Get uploaded file part + Part filePart = request.getPart("file"); + + if (filePart == null || filePart.getSize() == 0) { + response.sendRedirect(request.getContextPath() + "/?error=nofile"); + return; + } + + // Get file info + String originalFileName = getFileName(filePart); + String contentType = filePart.getContentType(); + long fileSize = filePart.getSize(); + + // Validate file type + if (!isValidImageType(contentType)) { + response.sendRedirect(request.getContextPath() + "/?error=invalidtype"); + return; + } + + // Read file data + byte[] photoData = readAllBytes(filePart.getInputStream()); + + // Generate stored filename + String storedFileName = UUID.randomUUID().toString() + getFileExtension(originalFileName); + String filePath = "/uploads/" + storedFileName; + + // Create Photo object + Photo photo = new Photo(); + photo.setOriginalFileName(originalFileName); + photo.setPhotoData(photoData); + photo.setStoredFileName(storedFileName); + photo.setFilePath(filePath); + photo.setFileSize(fileSize); + photo.setMimeType(contentType); + + // Get image dimensions + try { + BufferedImage image = ImageIO.read(new ByteArrayInputStream(photoData)); + if (image != null) { + photo.setWidth(image.getWidth()); + photo.setHeight(image.getHeight()); + } + } catch (Exception e) { + log("Could not read image dimensions", e); + } + + // Save to database + photoDAO.save(photo); + + // Redirect to home page with success message + response.sendRedirect(request.getContextPath() + "/?success=uploaded"); + + } catch (SQLException e) { + log("Error uploading photo", e); + response.sendRedirect(request.getContextPath() + "/?error=database"); + } + } + + /** + * Extract filename from Part header + */ + private String getFileName(Part part) { + String contentDisposition = part.getHeader("content-disposition"); + + for (String token : contentDisposition.split(";")) { + if (token.trim().startsWith("filename")) { + return token.substring(token.indexOf('=') + 1).trim() + .replace("\"", ""); + } + } + + return "unknown"; + } + + /** + * Get file extension from filename + */ + private String getFileExtension(String filename) { + int lastDot = filename.lastIndexOf('.'); + if (lastDot > 0) { + return filename.substring(lastDot); + } + return ""; + } + + /** + * Validate image MIME type + */ + private boolean isValidImageType(String mimeType) { + return mimeType != null && ( + mimeType.equals("image/jpeg") || + mimeType.equals("image/jpg") || + mimeType.equals("image/png") || + mimeType.equals("image/gif") || + mimeType.equals("image/webp") + ); + } + + /** + * Read all bytes from InputStream (Java 8 compatible) + */ + private byte[] readAllBytes(InputStream inputStream) throws IOException { + byte[] buffer = new byte[8192]; + int bytesRead; + java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream(); + + while ((bytesRead = inputStream.read(buffer)) != -1) { + output.write(buffer, 0, bytesRead); + } + + return output.toByteArray(); + } +} diff --git a/src/com/photoalbum/util/DatabaseConfig.java b/src/com/photoalbum/util/DatabaseConfig.java new file mode 100644 index 0000000..aca0bc5 --- /dev/null +++ b/src/com/photoalbum/util/DatabaseConfig.java @@ -0,0 +1,134 @@ +package com.photoalbum.util; + +import java.io.IOException; +import java.io.InputStream; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Properties; + +/** + * Database configuration and connection management + * Reads database properties from application.properties + */ +public class DatabaseConfig { + + private static String url; + private static String username; + private static String password; + private static String driverClassName; + + static { + loadProperties(); + initializeDatabase(); + } + + /** + * Load database properties from application.properties + */ + private static void loadProperties() { + Properties props = new Properties(); + + try (InputStream input = DatabaseConfig.class.getClassLoader() + .getResourceAsStream("application.properties")) { + + if (input == null) { + System.err.println("Unable to find application.properties"); + return; + } + + props.load(input); + + // Read properties + url = props.getProperty("app.datasource.url"); + username = props.getProperty("app.datasource.username"); + password = props.getProperty("app.datasource.password"); + driverClassName = props.getProperty("app.datasource.driver-class-name"); + + // Load JDBC driver + if (driverClassName != null && !driverClassName.isEmpty()) { + try { + Class.forName(driverClassName); + System.out.println("JDBC Driver loaded: " + driverClassName); + } catch (ClassNotFoundException e) { + System.err.println("JDBC Driver not found: " + driverClassName); + e.printStackTrace(); + } + } + + } catch (IOException e) { + System.err.println("Error loading application.properties"); + e.printStackTrace(); + } + } + + /** + * Get a database connection + */ + public static Connection getConnection() throws SQLException { + if (url == null || username == null || password == null) { + throw new SQLException("Database configuration not loaded properly"); + } + + return DriverManager.getConnection(url, username, password); + } + + /** + * Test database connection + */ + public static boolean testConnection() { + try (Connection conn = getConnection()) { + return conn != null && !conn.isClosed(); + } catch (SQLException e) { + System.err.println("Database connection test failed: " + e.getMessage()); + return false; + } + } + + // Getters + public static String getUrl() { + return url; + } + + public static String getUsername() { + return username; + } + + public static String getDriverClassName() { + return driverClassName; + } + + /** + * Initialize database schema for H2 (auto-create tables) + */ + private static void initializeDatabase() { + // Only initialize if using H2 + if (url != null && url.contains("jdbc:h2")) { + try (Connection conn = getConnection()) { + String createTableSQL = + "CREATE TABLE IF NOT EXISTS photos (" + + " id VARCHAR(36) PRIMARY KEY," + + " original_file_name VARCHAR(255) NOT NULL," + + " photo_data BLOB," + + " stored_file_name VARCHAR(255) NOT NULL," + + " file_path VARCHAR(500)," + + " file_size BIGINT NOT NULL," + + " mime_type VARCHAR(50) NOT NULL," + + " uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL," + + " width INT," + + " height INT" + + ")"; + + conn.createStatement().execute(createTableSQL); + + String createIndexSQL = "CREATE INDEX IF NOT EXISTS idx_photos_uploaded_at ON photos(uploaded_at)"; + conn.createStatement().execute(createIndexSQL); + + System.out.println("H2 database initialized successfully"); + } catch (SQLException e) { + System.err.println("Failed to initialize H2 database: " + e.getMessage()); + e.printStackTrace(); + } + } + } +} diff --git a/src/main/java/com/photoalbum/PhotoAlbumApplication.java b/src/main/java/com/photoalbum/PhotoAlbumApplication.java deleted file mode 100644 index 528f602..0000000 --- a/src/main/java/com/photoalbum/PhotoAlbumApplication.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.photoalbum; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -/** - * Main application class for the Photo Album application - */ -@SpringBootApplication -public class PhotoAlbumApplication { - - public static void main(String[] args) { - SpringApplication.run(PhotoAlbumApplication.class, args); - } -} \ No newline at end of file diff --git a/src/main/java/com/photoalbum/controller/DetailController.java b/src/main/java/com/photoalbum/controller/DetailController.java deleted file mode 100644 index d138ef9..0000000 --- a/src/main/java/com/photoalbum/controller/DetailController.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.photoalbum.controller; - -import com.photoalbum.model.Photo; -import com.photoalbum.service.PhotoService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import java.util.Optional; - -/** - * Controller for displaying a single photo in full size - */ -@Controller -@RequestMapping("/detail") -public class DetailController { - - private static final Logger logger = LoggerFactory.getLogger(DetailController.class); - - private final PhotoService photoService; - - public DetailController(PhotoService photoService) { - this.photoService = photoService; - } - - /** - * Handles GET requests to display a photo - */ - @GetMapping("/{id}") - public String detail(@PathVariable String id, Model model) { - if (id == null || id.trim().isEmpty()) { - return "redirect:/"; - } - - try { - Optional photoOpt = photoService.getPhotoById(id); - if (!photoOpt.isPresent()) { - return "redirect:/"; - } - - Photo photo = photoOpt.get(); - model.addAttribute("photo", photo); - - // Find previous and next photos for navigation - Optional previousPhoto = photoService.getPreviousPhoto(photo); - Optional nextPhoto = photoService.getNextPhoto(photo); - - model.addAttribute("previousPhotoId", previousPhoto.isPresent() ? previousPhoto.get().getId() : null); - model.addAttribute("nextPhotoId", nextPhoto.isPresent() ? nextPhoto.get().getId() : null); - - return "detail"; - } catch (Exception ex) { - logger.error("Error loading photo with ID {}", id, ex); - return "redirect:/"; - } - } - - /** - * Handles POST requests to delete a photo - */ - @PostMapping("/{id}/delete") - public String deletePhoto(@PathVariable String id, RedirectAttributes redirectAttributes) { - try { - boolean deleted = photoService.deletePhoto(id); - if (deleted) { - logger.info("Photo {} deleted successfully", id); - redirectAttributes.addFlashAttribute("successMessage", "Photo deleted successfully"); - } else { - redirectAttributes.addFlashAttribute("errorMessage", "Photo not found"); - } - } catch (Exception ex) { - logger.error("Error deleting photo {}", id, ex); - redirectAttributes.addFlashAttribute("errorMessage", "Failed to delete photo. Please try again."); - } - return "redirect:/"; - } -} \ No newline at end of file diff --git a/src/main/java/com/photoalbum/controller/HomeController.java b/src/main/java/com/photoalbum/controller/HomeController.java deleted file mode 100644 index d9905d9..0000000 --- a/src/main/java/com/photoalbum/controller/HomeController.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.photoalbum.controller; - -import com.photoalbum.model.Photo; -import com.photoalbum.model.UploadResult; -import com.photoalbum.service.PhotoService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -/** - * Controller for the main photo gallery page with upload functionality - */ -@Controller -public class HomeController { - - private static final Logger logger = LoggerFactory.getLogger(HomeController.class); - - private final PhotoService photoService; - - public HomeController(PhotoService photoService) { - this.photoService = photoService; - } - - /** - * Handler for GET requests - loads all photos for display - */ - @GetMapping("/") - public String index(Model model) { - try { - List photos = photoService.getAllPhotos(); - model.addAttribute("photos", photos); - // Add timestamp for cache busting - model.addAttribute("timestamp", System.currentTimeMillis()); - } catch (Exception ex) { - logger.error("Error loading photos", ex); - model.addAttribute("photos", new ArrayList()); - model.addAttribute("timestamp", System.currentTimeMillis()); - } - return "index"; - } - - /** - * Handler for POST requests - uploads one or more photo files - */ - @PostMapping("/upload") - @ResponseBody - public ResponseEntity> uploadPhotos(@RequestParam("files") List files) { - Map response = new HashMap(); - List> uploadedPhotos = new ArrayList>(); - List> failedUploads = new ArrayList>(); - - if (files == null || files.isEmpty()) { - response.put("success", false); - response.put("error", "No files provided"); - return ResponseEntity.badRequest().body(response); - } - - for (MultipartFile file : files) { - UploadResult result = photoService.uploadPhoto(file); - - if (result.isSuccess()) { - Optional photoOpt = photoService.getPhotoById(result.getPhotoId()); - if (photoOpt.isPresent()) { - Photo photo = photoOpt.get(); - Map uploadedPhoto = new HashMap(); - uploadedPhoto.put("id", photo.getId()); - uploadedPhoto.put("originalFileName", photo.getOriginalFileName()); - uploadedPhoto.put("filePath", photo.getFilePath()); - uploadedPhoto.put("uploadedAt", photo.getUploadedAt()); - uploadedPhoto.put("fileSize", photo.getFileSize()); - uploadedPhoto.put("width", photo.getWidth()); - uploadedPhoto.put("height", photo.getHeight()); - uploadedPhotos.add(uploadedPhoto); - } - } else { - Map failedUpload = new HashMap(); - failedUpload.put("fileName", result.getFileName()); - failedUpload.put("error", result.getErrorMessage()); - failedUploads.add(failedUpload); - } - } - - response.put("success", !uploadedPhotos.isEmpty()); - response.put("uploadedPhotos", uploadedPhotos); - response.put("failedUploads", failedUploads); - - return ResponseEntity.ok(response); - } -} \ No newline at end of file diff --git a/src/main/java/com/photoalbum/controller/PhotoFileController.java b/src/main/java/com/photoalbum/controller/PhotoFileController.java deleted file mode 100644 index 2f31408..0000000 --- a/src/main/java/com/photoalbum/controller/PhotoFileController.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.photoalbum.controller; - -import com.photoalbum.model.Photo; -import com.photoalbum.service.PhotoService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; - -import java.util.Optional; - -/** - * Controller for serving photo files from Oracle database BLOB storage - */ -@Controller -@RequestMapping("/photo") -public class PhotoFileController { - - private static final Logger logger = LoggerFactory.getLogger(PhotoFileController.class); - - private final PhotoService photoService; - - public PhotoFileController(PhotoService photoService) { - this.photoService = photoService; - } - - /** - * Serves a photo file by ID from Oracle database BLOB storage - */ - @GetMapping("/{id}") - public ResponseEntity servePhoto(@PathVariable String id) { - if (id == null || id.trim().isEmpty()) { - logger.warn("Photo file request with null or empty ID"); - return ResponseEntity.notFound().build(); - } - - try { - logger.info("=== DEBUGGING: Serving photo request for ID {} ===", id); - Optional photoOpt = photoService.getPhotoById(id); - - if (!photoOpt.isPresent()) { - logger.warn("Photo with ID {} not found", id); - return ResponseEntity.notFound().build(); - } - - Photo photo = photoOpt.get(); - logger.info("Found photo: originalFileName={}, mimeType={}", - photo.getOriginalFileName(), photo.getMimeType()); - - // Get photo data from Oracle database BLOB - byte[] photoData = photo.getPhotoData(); - if (photoData == null || photoData.length == 0) { - logger.error("No photo data found for photo ID {}", id); - return ResponseEntity.notFound().build(); - } - - logger.info("Photo data retrieved: {} bytes, first 10 bytes: {}", - photoData.length, - photoData.length >= 10 ? java.util.Arrays.toString(java.util.Arrays.copyOf(photoData, 10)) : "less than 10 bytes"); - - // Create resource from byte array - Resource resource = new ByteArrayResource(photoData); - - logger.info("Serving photo ID {} ({}, {} bytes) from Oracle database", - id, photo.getOriginalFileName(), photoData.length); - - // Return the photo data with appropriate content type and aggressive no-cache headers - return ResponseEntity.ok() - .contentType(MediaType.parseMediaType(photo.getMimeType())) - .header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate, private") - .header(HttpHeaders.PRAGMA, "no-cache") - .header(HttpHeaders.EXPIRES, "0") - .header("X-Photo-ID", String.valueOf(id)) - .header("X-Photo-Name", photo.getOriginalFileName()) - .header("X-Photo-Size", String.valueOf(photoData.length)) - .body(resource); - } catch (Exception ex) { - logger.error("Error serving photo with ID {} from Oracle database", id, ex); - return ResponseEntity.status(500).build(); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/photoalbum/repository/PhotoRepository.java b/src/main/java/com/photoalbum/repository/PhotoRepository.java deleted file mode 100644 index 135799a..0000000 --- a/src/main/java/com/photoalbum/repository/PhotoRepository.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.photoalbum.repository; - -import com.photoalbum.model.Photo; -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.time.LocalDateTime; -import java.util.List; - -/** - * Repository interface for Photo entity operations - */ -@Repository -public interface PhotoRepository extends JpaRepository { - - /** - * Find all photos ordered by upload date (newest first) - * @return List of photos ordered by upload date descending - */ - @Query(value = "SELECT ID, ORIGINAL_FILE_NAME, PHOTO_DATA, STORED_FILE_NAME, FILE_PATH, FILE_SIZE, " + - "MIME_TYPE, UPLOADED_AT, WIDTH, HEIGHT " + - "FROM PHOTOS " + - "ORDER BY UPLOADED_AT DESC", - nativeQuery = true) - List findAllOrderByUploadedAtDesc(); - - /** - * Find photos uploaded before a specific photo (for navigation) - * @param uploadedAt The upload timestamp to compare against - * @return List of photos uploaded before the given timestamp - */ - @Query(value = "SELECT * FROM (" + - "SELECT ID, ORIGINAL_FILE_NAME, PHOTO_DATA, STORED_FILE_NAME, FILE_PATH, FILE_SIZE, " + - "MIME_TYPE, UPLOADED_AT, WIDTH, HEIGHT, ROWNUM as RN " + - "FROM PHOTOS " + - "WHERE UPLOADED_AT < :uploadedAt " + - "ORDER BY UPLOADED_AT DESC" + - ") WHERE ROWNUM <= 10", - nativeQuery = true) - List findPhotosUploadedBefore(@Param("uploadedAt") LocalDateTime uploadedAt); - - /** - * Find photos uploaded after a specific photo (for navigation) - * @param uploadedAt The upload timestamp to compare against - * @return List of photos uploaded after the given timestamp - */ - @Query(value = "SELECT ID, ORIGINAL_FILE_NAME, PHOTO_DATA, STORED_FILE_NAME, " + - "NVL(FILE_PATH, 'default_path') as FILE_PATH, FILE_SIZE, " + - "MIME_TYPE, UPLOADED_AT, WIDTH, HEIGHT " + - "FROM PHOTOS " + - "WHERE UPLOADED_AT > :uploadedAt " + - "ORDER BY UPLOADED_AT ASC", - nativeQuery = true) - List findPhotosUploadedAfter(@Param("uploadedAt") LocalDateTime uploadedAt); - - /** - * Find photos by upload month using Oracle TO_CHAR function - Oracle specific - * @param year The year to search for - * @param month The month to search for - * @return List of photos uploaded in the specified month - */ - @Query(value = "SELECT ID, ORIGINAL_FILE_NAME, PHOTO_DATA, STORED_FILE_NAME, FILE_PATH, FILE_SIZE, " + - "MIME_TYPE, UPLOADED_AT, WIDTH, HEIGHT " + - "FROM PHOTOS " + - "WHERE TO_CHAR(UPLOADED_AT, 'YYYY') = :year " + - "AND TO_CHAR(UPLOADED_AT, 'MM') = :month " + - "ORDER BY UPLOADED_AT DESC", - nativeQuery = true) - List findPhotosByUploadMonth(@Param("year") String year, @Param("month") String month); - - /** - * Get paginated photos using Oracle ROWNUM - Oracle specific pagination - * @param startRow Starting row number (1-based) - * @param endRow Ending row number - * @return List of photos within the specified row range - */ - @Query(value = "SELECT * FROM (" + - "SELECT P.*, ROWNUM as RN FROM (" + - "SELECT ID, ORIGINAL_FILE_NAME, PHOTO_DATA, STORED_FILE_NAME, FILE_PATH, FILE_SIZE, " + - "MIME_TYPE, UPLOADED_AT, WIDTH, HEIGHT " + - "FROM PHOTOS ORDER BY UPLOADED_AT DESC" + - ") P WHERE ROWNUM <= :endRow" + - ") WHERE RN >= :startRow", - nativeQuery = true) - List findPhotosWithPagination(@Param("startRow") int startRow, @Param("endRow") int endRow); - - /** - * Find photos with file size statistics using Oracle analytical functions - Oracle specific - * @return List of photos with running totals and rankings - */ - @Query(value = "SELECT ID, ORIGINAL_FILE_NAME, PHOTO_DATA, STORED_FILE_NAME, FILE_PATH, FILE_SIZE, " + - "MIME_TYPE, UPLOADED_AT, WIDTH, HEIGHT, " + - "RANK() OVER (ORDER BY FILE_SIZE DESC) as SIZE_RANK, " + - "SUM(FILE_SIZE) OVER (ORDER BY UPLOADED_AT ROWS UNBOUNDED PRECEDING) as RUNNING_TOTAL " + - "FROM PHOTOS " + - "ORDER BY UPLOADED_AT DESC", - nativeQuery = true) - List findPhotosWithStatistics(); -} \ No newline at end of file diff --git a/src/main/java/com/photoalbum/service/PhotoService.java b/src/main/java/com/photoalbum/service/PhotoService.java deleted file mode 100644 index 3c2ad76..0000000 --- a/src/main/java/com/photoalbum/service/PhotoService.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.photoalbum.service; - -import com.photoalbum.model.Photo; -import com.photoalbum.model.UploadResult; -import org.springframework.web.multipart.MultipartFile; - -import java.util.List; -import java.util.Optional; - -/** - * Service interface for photo operations - */ -public interface PhotoService { - - /** - * Get all photos ordered by upload date (newest first) - * @return List of photos - */ - List getAllPhotos(); - - /** - * Get a specific photo by ID - * @param id Photo ID - * @return Photo if found, empty otherwise - */ - Optional getPhotoById(String id); - - /** - * Upload a photo file - * @param file The uploaded file - * @return Upload result with success status and photo details or error message - */ - UploadResult uploadPhoto(MultipartFile file); - - /** - * Delete a photo by ID - * @param id Photo ID - * @return True if deleted successfully, false if not found - */ - boolean deletePhoto(String id); - - /** - * Get the previous photo (older) for navigation - * @param currentPhoto The current photo - * @return Previous photo if found, empty otherwise - */ - Optional getPreviousPhoto(Photo currentPhoto); - - /** - * Get the next photo (newer) for navigation - * @param currentPhoto The current photo - * @return Next photo if found, empty otherwise - */ - Optional getNextPhoto(Photo currentPhoto); -} \ No newline at end of file diff --git a/src/main/java/com/photoalbum/service/impl/PhotoServiceImpl.java b/src/main/java/com/photoalbum/service/impl/PhotoServiceImpl.java deleted file mode 100644 index fa379a5..0000000 --- a/src/main/java/com/photoalbum/service/impl/PhotoServiceImpl.java +++ /dev/null @@ -1,229 +0,0 @@ -package com.photoalbum.service.impl; - -import com.photoalbum.model.Photo; -import com.photoalbum.model.UploadResult; -import com.photoalbum.repository.PhotoRepository; -import com.photoalbum.service.PhotoService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -/** - * Service implementation for photo operations including upload, retrieval, and deletion - */ -@Service -@Transactional -public class PhotoServiceImpl implements PhotoService { - - private static final Logger logger = LoggerFactory.getLogger(PhotoServiceImpl.class); - - private final PhotoRepository photoRepository; - private final long maxFileSizeBytes; - private final List allowedMimeTypes; - - public PhotoServiceImpl( - PhotoRepository photoRepository, - @Value("${app.file-upload.max-file-size-bytes}") long maxFileSizeBytes, - @Value("${app.file-upload.allowed-mime-types}") String[] allowedMimeTypes) { - this.photoRepository = photoRepository; - this.maxFileSizeBytes = maxFileSizeBytes; - this.allowedMimeTypes = Arrays.asList(allowedMimeTypes); - } - - /** - * Get all photos ordered by upload date (newest first) - */ - @Override - @Transactional(readOnly = true) - public List getAllPhotos() { - try { - return photoRepository.findAllOrderByUploadedAtDesc(); - } catch (Exception ex) { - logger.error("Error retrieving photos from database", ex); - throw new RuntimeException("Error retrieving photos", ex); - } - } - - /** - * Get a specific photo by ID - */ - @Override - @Transactional(readOnly = true) - public Optional getPhotoById(String id) { - try { - return photoRepository.findById(id); - } catch (Exception ex) { - logger.error("Error retrieving photo with ID {}", id, ex); - throw new RuntimeException("Error retrieving photo", ex); - } - } - - /** - * Upload a photo file - */ - @Override - public UploadResult uploadPhoto(MultipartFile file) { - UploadResult result = new UploadResult(); - result.setFileName(file.getOriginalFilename()); - - try { - // Validate file type - if (!allowedMimeTypes.contains(file.getContentType().toLowerCase())) { - result.setSuccess(false); - result.setErrorMessage("File type not supported. Please upload JPEG, PNG, GIF, or WebP images."); - logger.warn("Upload rejected: Invalid file type {} for {}", - file.getContentType(), file.getOriginalFilename()); - return result; - } - - // Validate file size - if (file.getSize() > maxFileSizeBytes) { - result.setSuccess(false); - result.setErrorMessage(String.format("File size exceeds %dMB limit.", maxFileSizeBytes / 1024 / 1024)); - logger.warn("Upload rejected: File size {} exceeds limit for {}", - file.getSize(), file.getOriginalFilename()); - return result; - } - - // Validate file length - if (file.getSize() <= 0) { - result.setSuccess(false); - result.setErrorMessage("File is empty."); - return result; - } - - // Generate unique filename for compatibility (stored in database, not on disk) - String extension = getFileExtension(file.getOriginalFilename()); - String storedFileName = UUID.randomUUID().toString() + extension; - String relativePath = "/uploads/" + storedFileName; // For compatibility only - - // Extract image dimensions and read file data - Integer width = null; - Integer height = null; - byte[] photoData = null; - - try { - // Read file content for database storage - photoData = file.getBytes(); - - // Extract image dimensions from byte array - try (ByteArrayInputStream bis = new ByteArrayInputStream(photoData)) { - BufferedImage image = ImageIO.read(bis); - if (image != null) { - width = image.getWidth(); - height = image.getHeight(); - } - } - } catch (IOException ex) { - logger.error("Error reading file data for {}", file.getOriginalFilename(), ex); - result.setSuccess(false); - result.setErrorMessage("Error reading file data. Please try again."); - return result; - } catch (Exception ex) { - logger.warn("Could not extract image dimensions for {}", file.getOriginalFilename(), ex); - // Continue without dimensions - not critical - } - - // Create photo entity with database BLOB storage - Photo photo = new Photo( - file.getOriginalFilename(), - photoData, // Store actual photo data in Oracle database - storedFileName, - relativePath, // Keep for compatibility, not used for serving - file.getSize(), - file.getContentType() - ); - photo.setWidth(width); - photo.setHeight(height); - - // Save to database (with BLOB photo data) - try { - photo = photoRepository.save(photo); - - result.setSuccess(true); - result.setPhotoId(photo.getId()); - - logger.info("Successfully uploaded photo {} with ID {} to Oracle database", - file.getOriginalFilename(), photo.getId()); - } catch (Exception ex) { - logger.error("Error saving photo to Oracle database for {}", file.getOriginalFilename(), ex); - result.setSuccess(false); - result.setErrorMessage("Error saving photo to database. Please try again."); - } - } catch (Exception ex) { - logger.error("Unexpected error during photo upload for {}", file.getOriginalFilename(), ex); - result.setSuccess(false); - result.setErrorMessage("An unexpected error occurred. Please try again."); - } - - return result; - } - - /** - * Delete a photo by ID - */ - @Override - public boolean deletePhoto(String id) { - try { - Optional photoOpt = photoRepository.findById(id); - if (!photoOpt.isPresent()) { - logger.warn("Photo with ID {} not found for deletion", id); - return false; - } - - Photo photo = photoOpt.get(); - - // Delete from Oracle database (photos stored as BLOB) - photoRepository.delete(photo); - - logger.info("Successfully deleted photo ID {} from Oracle database", id); - return true; - } catch (Exception ex) { - logger.error("Error deleting photo with ID {} from Oracle database", id, ex); - throw new RuntimeException("Error deleting photo", ex); - } - } - - /** - * Get the previous photo (older) for navigation - */ - @Override - @Transactional(readOnly = true) - public Optional getPreviousPhoto(Photo currentPhoto) { - List olderPhotos = photoRepository.findPhotosUploadedBefore(currentPhoto.getUploadedAt()); - return olderPhotos.isEmpty() ? Optional.empty() : Optional.of(olderPhotos.get(0)); - } - - /** - * Get the next photo (newer) for navigation - */ - @Override - @Transactional(readOnly = true) - public Optional getNextPhoto(Photo currentPhoto) { - List newerPhotos = photoRepository.findPhotosUploadedAfter(currentPhoto.getUploadedAt()); - return newerPhotos.isEmpty() ? Optional.empty() : Optional.of(newerPhotos.get(0)); - } - - /** - * Extract file extension from filename - */ - private String getFileExtension(String filename) { - if (filename == null || filename.isEmpty()) { - return ""; - } - int lastDotIndex = filename.lastIndexOf('.'); - return lastDotIndex > 0 ? filename.substring(lastDotIndex) : ""; - } -} \ No newline at end of file diff --git a/src/main/java/com/photoalbum/util/MathUtil.java b/src/main/java/com/photoalbum/util/MathUtil.java deleted file mode 100644 index 81378d2..0000000 --- a/src/main/java/com/photoalbum/util/MathUtil.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.photoalbum.util; - -/** - * Mathematical utility functions - */ -public class MathUtil { - - /** - * Calculate the Greatest Common Divisor (GCD) of two integers - * @param a First integer - * @param b Second integer - * @return The GCD of a and b - */ - public static int gcd(int a, int b) { - while (b != 0) { - int temp = b; - b = a % b; - a = temp; - } - return a; - } -} \ No newline at end of file diff --git a/src/main/resources/application-docker.properties b/src/main/resources/application-docker.properties deleted file mode 100644 index 8dff306..0000000 --- a/src/main/resources/application-docker.properties +++ /dev/null @@ -1,34 +0,0 @@ -# Docker-specific configuration for Oracle DB -spring.datasource.url=jdbc:oracle:thin:@oracle-db:1521:XE -spring.datasource.username=photoalbum -spring.datasource.password=photoalbum -spring.datasource.driver-class-name=oracle.jdbc.OracleDriver - -# Character encoding -server.servlet.encoding.charset=UTF-8 -server.servlet.encoding.enabled=true -server.servlet.encoding.force=true - -# JPA Configuration for Docker -spring.jpa.database-platform=org.hibernate.dialect.OracleDialect -spring.jpa.hibernate.ddl-auto=create -spring.jpa.show-sql=true -spring.jpa.properties.hibernate.format_sql=true - -# File Upload Configuration - Validation only (photos stored in Oracle database) -app.file-upload.max-file-size-bytes=10485760 -app.file-upload.allowed-mime-types=image/jpeg,image/png,image/gif,image/webp -app.file-upload.max-files-per-upload=10 - -# Other settings remain the same -server.port=8080 -spring.servlet.multipart.max-file-size=10MB -spring.servlet.multipart.max-request-size=50MB -app.file-upload.max-file-size-bytes=10485760 -app.file-upload.allowed-mime-types=image/jpeg,image/png,image/gif,image/webp -app.file-upload.max-files-per-upload=10 - -# Logging for Docker -logging.level.com.photoalbum=INFO -logging.level.org.springframework.web=WARN -logging.level.org.hibernate.SQL=DEBUG \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index cca6b44..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,32 +0,0 @@ -# Spring Boot Configuration -server.port=8080 - -# Character encoding -server.servlet.encoding.charset=UTF-8 -server.servlet.encoding.enabled=true -server.servlet.encoding.force=true - -# Oracle Database Configuration -spring.datasource.url=jdbc:oracle:thin:@oracle-db:1521/FREEPDB1 -spring.datasource.username=photoalbum -spring.datasource.password=photoalbum -spring.datasource.driver-class-name=oracle.jdbc.OracleDriver - -# JPA Configuration -spring.jpa.database-platform=org.hibernate.dialect.OracleDialect -spring.jpa.hibernate.ddl-auto=create -spring.jpa.show-sql=true -spring.jpa.properties.hibernate.format_sql=true - -# File Upload Configuration -spring.servlet.multipart.max-file-size=10MB -spring.servlet.multipart.max-request-size=50MB - -# Application Configuration - File Upload Validation -app.file-upload.max-file-size-bytes=10485760 -app.file-upload.allowed-mime-types=image/jpeg,image/png,image/gif,image/webp -app.file-upload.max-files-per-upload=10 - -# Logging -logging.level.com.photoalbum=DEBUG -logging.level.org.springframework.web=DEBUG diff --git a/src/main/resources/static/js/upload.js b/src/main/resources/static/js/upload.js deleted file mode 100644 index 41c7b0e..0000000 --- a/src/main/resources/static/js/upload.js +++ /dev/null @@ -1,221 +0,0 @@ -// Photo Album Upload JavaScript -(function () { - 'use strict'; - - const dropZone = document.getElementById('drop-zone'); - const fileInput = document.getElementById('file-input'); - const uploadForm = document.getElementById('upload-form'); - const uploadFeedback = document.getElementById('upload-feedback'); - const uploadProgress = document.getElementById('upload-progress'); - const uploadSuccess = document.getElementById('upload-success'); - const uploadErrors = document.getElementById('upload-errors'); - const photoGallery = document.getElementById('photo-gallery'); - - if (!dropZone || !fileInput) { - console.error('Required elements not found'); - return; - } - - // Click on drop zone to open file picker - dropZone.addEventListener('click', () => { - fileInput.click(); - }); - - // Prevent default drag behaviors - ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { - dropZone.addEventListener(eventName, preventDefaults, false); - document.body.addEventListener(eventName, preventDefaults, false); - }); - - function preventDefaults(e) { - e.preventDefault(); - e.stopPropagation(); - } - - // Highlight drop zone when dragging over it - ['dragenter', 'dragover'].forEach(eventName => { - dropZone.addEventListener(eventName, () => { - dropZone.classList.add('drop-zone-highlight'); - }, false); - }); - - ['dragleave', 'drop'].forEach(eventName => { - dropZone.addEventListener(eventName, () => { - dropZone.classList.remove('drop-zone-highlight'); - }, false); - }); - - // Handle dropped files - dropZone.addEventListener('drop', (e) => { - const dt = e.dataTransfer; - const files = dt.files; - handleFiles(files); - }, false); - - // Handle file input change - fileInput.addEventListener('change', (e) => { - handleFiles(e.target.files); - }); - - function handleFiles(files) { - if (!files || files.length === 0) { - return; - } - - // Client-side validation - const validFiles = []; - const errors = []; - const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; - const maxSize = 10 * 1024 * 1024; // 10MB - - Array.from(files).forEach(file => { - if (!allowedTypes.includes(file.type)) { - errors.push(`${file.name}: File type not supported. Please upload JPEG, PNG, GIF, or WebP images.`); - } else if (file.size > maxSize) { - errors.push(`${file.name}: File size exceeds 10MB limit.`); - } else { - validFiles.push(file); - } - }); - - if (errors.length > 0) { - showErrors(errors); - } - - if (validFiles.length > 0) { - uploadFiles(validFiles); - } - } - - async function uploadFiles(files) { - // Show progress - uploadFeedback.classList.remove('d-none'); - uploadProgress.classList.remove('d-none'); - uploadSuccess.classList.add('d-none'); - uploadErrors.classList.add('d-none'); - - const formData = new FormData(); - files.forEach(file => { - formData.append('files', file); - }); - - try { - const response = await fetch('/upload', { - method: 'POST', - body: formData - }); - - uploadProgress.classList.add('d-none'); - - if (response.ok) { - const result = await response.json(); - - if (result.uploadedPhotos && result.uploadedPhotos.length > 0) { - showSuccess(`Successfully uploaded ${result.uploadedPhotos.length} photo(s)!`); - displayNewPhotos(result.uploadedPhotos); - } - - if (result.failedUploads && result.failedUploads.length > 0) { - const errorMessages = result.failedUploads.map(f => `${f.fileName}: ${f.error}`); - showErrors(errorMessages); - } - - // Reset file input - fileInput.value = ''; - } else { - showErrors(['Upload failed. Please try again.']); - } - } catch (error) { - uploadProgress.classList.add('d-none'); - console.error('Upload error:', error); - showErrors(['An error occurred during upload. Please try again.']); - } - } - - function displayNewPhotos(photos) { - // Remove "no photos" message if it exists - const alertInfo = document.querySelector('#gallery-section .alert-info'); - if (alertInfo) { - alertInfo.remove(); - } - - // Get the current gallery element (may have been created dynamically) - let galleryElement = document.getElementById('photo-gallery'); - - // Create gallery if it doesn't exist - if (!galleryElement) { - const gallerySection = document.getElementById('gallery-section'); - galleryElement = document.createElement('div'); - galleryElement.className = 'row'; - galleryElement.id = 'photo-gallery'; - gallerySection.appendChild(galleryElement); - } - - // Add photos to the beginning of the gallery - photos.forEach((photo) => { - const photoCard = createPhotoCard(photo); - galleryElement.insertAdjacentHTML('afterbegin', photoCard); - }); - } - - function createPhotoCard(photo) { - const uploadDate = new Date(photo.uploadedAt); - const formattedDate = uploadDate.toLocaleString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: true - }); - - const dimensions = (photo.width && photo.height) - ? ` • ${photo.width} x ${photo.height}` - : ''; - - // Use photo URL with current timestamp to bypass all caching - const timestamp = new Date().getTime(); - const photoUrl = `/photo/${photo.id}?_t=${timestamp}`; - const detailUrl = `/detail/${photo.id}`; - - return ` -
-
- - ${photo.originalFileName} - -
-

- ${photo.originalFileName} -

-

- ${formattedDate} -

-

- - ${Math.round(photo.fileSize / 1024)} KB${dimensions} - -

-
-
-
- `; - } - - function showSuccess(message) { - uploadSuccess.textContent = message; - uploadSuccess.classList.remove('d-none'); - - // Auto-hide after 5 seconds - setTimeout(() => { - uploadSuccess.classList.add('d-none'); - }, 5000); - } - - function showErrors(errors) { - uploadErrors.innerHTML = 'Upload errors:
    ' + - errors.map(e => `
  • ${e}
  • `).join('') + - '
'; - uploadErrors.classList.remove('d-none'); - } -})(); \ No newline at end of file diff --git a/src/main/resources/static/uploads/5c62a4c2-d4eb-446f-9cd2-7ab8898cf371.jpg b/src/main/resources/static/uploads/5c62a4c2-d4eb-446f-9cd2-7ab8898cf371.jpg deleted file mode 100644 index 5bc4f8e..0000000 Binary files a/src/main/resources/static/uploads/5c62a4c2-d4eb-446f-9cd2-7ab8898cf371.jpg and /dev/null differ diff --git a/src/main/resources/templates/detail.html b/src/main/resources/templates/detail.html deleted file mode 100644 index 75aeecb..0000000 --- a/src/main/resources/templates/detail.html +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - Photo Detail - Photo Album - - - - -
- -
- -
-
-
-
-

Photo not found

-

The photo you're looking for doesn't exist or has been deleted.

- Back to Gallery -
-
- -
- -
- - - - - Back to Gallery - - -
- -
-
- - -
-
-
-
- -
-
- - - -
- - -
-
-
-
Photo Information
-
-
-
-
Filename:
-
- -
Uploaded:
-
-
- -
- -
File Size:
-
- - - -
- -
-
Dimensions:
-
-
- -
Type:
-
- -
-
-
-
-
-
-
-
-
- -
-
- © 2025 - Photo Album - A simple photo storage application -
-
- - - - \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html deleted file mode 100644 index 33c6b37..0000000 --- a/src/main/resources/templates/index.html +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - Photo Gallery - Photo Album - - - - -
- -
- -
-
-
-

📸 Photo Gallery

-

Upload and view your photos

-
- - - - - - - -
-
-
Upload Photos
-
-
-
- - - - -

Drag and drop photos here

-

or click to select files

-

Supports: JPEG, PNG, GIF, WebP (max 10MB each)

-
- -
- - -
-
-
- Uploading... -
- Uploading photos... -
-
-
-
-
-
-
- - - -
-
- -
-
- © 2025 - Photo Album - A simple photo storage application -
-
- - - - - \ No newline at end of file diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html deleted file mode 100644 index 54f7f3e..0000000 --- a/src/main/resources/templates/layout.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - Photo Album - - - - -
- -
-
-
- -
-
-
- -
-
- © 2025 - Photo Album - A simple photo storage application -
-
- - - - - - - - - - \ No newline at end of file diff --git a/src/test/java/com/photoalbum/PhotoAlbumApplicationTests.java b/src/test/java/com/photoalbum/PhotoAlbumApplicationTests.java deleted file mode 100644 index 6f710ba..0000000 --- a/src/test/java/com/photoalbum/PhotoAlbumApplicationTests.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.photoalbum; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -@SpringBootTest -@ActiveProfiles("test") -class PhotoAlbumApplicationTests { - - @Test - void contextLoads() { - // This test ensures that the Spring context loads correctly - } -} \ No newline at end of file diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties deleted file mode 100644 index 41da82b..0000000 --- a/src/test/resources/application-test.properties +++ /dev/null @@ -1,19 +0,0 @@ -# Test configuration - use H2 in-memory database for testing -spring.datasource.url=jdbc:h2:mem:testdb -spring.datasource.driver-class-name=org.h2.Driver -spring.datasource.username=sa -spring.datasource.password= - -# JPA Configuration for testing -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -spring.jpa.hibernate.ddl-auto=create-drop -spring.jpa.show-sql=false - -# File upload configuration for testing -app.file-upload.upload-path=target/test-uploads -app.file-upload.max-file-size-bytes=10485760 -app.file-upload.allowed-mime-types=image/jpeg,image/png,image/gif,image/webp -app.file-upload.max-files-per-upload=10 - -# Logging for tests -logging.level.com.photoalbum=DEBUG \ No newline at end of file diff --git a/webapp/WEB-INF/jsp/detail.jsp b/webapp/WEB-INF/jsp/detail.jsp new file mode 100644 index 0000000..71848a6 --- /dev/null +++ b/webapp/WEB-INF/jsp/detail.jsp @@ -0,0 +1,152 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> + + + + + + ${not empty photo ? fn:escapeXml(photo.originalFileName) : 'Photo Detail'} - Photo Album + + + + +
+ +
+ +
+
+ +
+

Photo not found

+

The photo you're looking for doesn't exist or has been deleted.

+ Back to Gallery +
+
+ + +
+ +
+ + + + + Back to Gallery + + +
+ + + +
+
+ + +
+
+
+
+ ${fn:escapeXml(photo.originalFileName)} +
+
+ + + +
+ + +
+
+
+
Photo Information
+
+
+
+
Filename:
+
${fn:escapeXml(photo.originalFileName)}
+ +
Uploaded:
+
+
+ +
+ +
File Size:
+
+ + + ${photo.fileSize} bytes + + + KB + + + MB + + +
+ + +
Dimensions:
+
${photo.width} x ${photo.height} px
+
+ +
Type:
+
+ ${photo.mimeType} +
+
+
+
+
+
+
+
+
+
+ +
+
+ © 2025 - Photo Album - A simple photo storage application +
+
+ + + + diff --git a/webapp/WEB-INF/jsp/index.jsp b/webapp/WEB-INF/jsp/index.jsp new file mode 100644 index 0000000..32e6285 --- /dev/null +++ b/webapp/WEB-INF/jsp/index.jsp @@ -0,0 +1,152 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> + + + + + + Photo Gallery - Photo Album + + + + +
+ +
+ +
+
+
+

📸 Photo Gallery

+

Upload and view your photos

+
+ + + + + + + + + + + + + + + +
+
+
Upload Photos
+
+
+
+ + + + +

Drag and drop photos here

+

or click to select files

+

Supports: JPEG, PNG, GIF, WebP (max 10MB each)

+
+ +
+ + +
+
+
+ + + +
+
+ +
+
+ © 2025 - Photo Album - A simple photo storage application +
+
+ + + + + diff --git a/webapp/WEB-INF/web.xml b/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..91379a0 --- /dev/null +++ b/webapp/WEB-INF/web.xml @@ -0,0 +1,80 @@ + + + + Photo Album Application + A simple photo gallery application using Servlet/JSP + + + + + + uploadDirectory + /uploads + + + + + + + + + HomeServlet + com.photoalbum.servlet.HomeServlet + + + HomeServlet + /index + + + HomeServlet + /home + + + + + UploadServlet + com.photoalbum.servlet.UploadServlet + + + 10485760 + 10485760 + 0 + + + + UploadServlet + /upload + + + + + DetailServlet + com.photoalbum.servlet.DetailServlet + + + DetailServlet + /detail + + + + + DownloadServlet + com.photoalbum.servlet.DownloadServlet + + + DownloadServlet + /download + + + + + + + 30 + + + diff --git a/src/main/resources/static/css/site.css b/webapp/css/site.css similarity index 100% rename from src/main/resources/static/css/site.css rename to webapp/css/site.css diff --git a/webapp/index.html b/webapp/index.html new file mode 100644 index 0000000..430d80c --- /dev/null +++ b/webapp/index.html @@ -0,0 +1,9 @@ + + + + + + + Redirecting to photo gallery... + + diff --git a/webapp/js/upload.js b/webapp/js/upload.js new file mode 100644 index 0000000..14420b8 --- /dev/null +++ b/webapp/js/upload.js @@ -0,0 +1,64 @@ +// Photo Album Upload JavaScript - Servlet Version +(function () { + 'use strict'; + + const dropZone = document.getElementById('drop-zone'); + const fileInput = document.getElementById('file-input'); + const uploadForm = document.getElementById('upload-form'); + const uploadBtn = document.getElementById('upload-btn'); + + if (!dropZone || !fileInput) { + console.error('Required elements not found'); + return; + } + + // Click on drop zone to open file picker + dropZone.addEventListener('click', () => { + fileInput.click(); + }); + + // Enable upload button when file is selected + fileInput.addEventListener('change', () => { + if (fileInput.files && fileInput.files.length > 0) { + uploadBtn.disabled = false; + } else { + uploadBtn.disabled = true; + } + }); + + // Prevent default drag behaviors + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + dropZone.addEventListener(eventName, preventDefaults, false); + document.body.addEventListener(eventName, preventDefaults, false); + }); + + function preventDefaults(e) { + e.preventDefault(); + e.stopPropagation(); + } + + // Highlight drop zone when dragging over it + ['dragenter', 'dragover'].forEach(eventName => { + dropZone.addEventListener(eventName, () => { + dropZone.classList.add('drop-zone-highlight'); + }, false); + }); + + ['dragleave', 'drop'].forEach(eventName => { + dropZone.addEventListener(eventName, () => { + dropZone.classList.remove('drop-zone-highlight'); + }, false); + }); + + // Handle dropped files + dropZone.addEventListener('drop', (e) => { + const dt = e.dataTransfer; + const files = dt.files; + + if (files && files.length > 0) { + fileInput.files = files; + uploadBtn.disabled = false; + } + }, false); + +})();