diff --git a/.cursor/rules/100-java-checklist-guide.mdc b/.cursor/rules/100-java-checklist-guide.mdc index 9a469a8d..fd5ccd77 100644 --- a/.cursor/rules/100-java-checklist-guide.mdc +++ b/.cursor/rules/100-java-checklist-guide.mdc @@ -1,57 +1,36 @@ --- -description: Create a Checklist with all Java steps to use with cursor rules for Java -globs: +description: +globs: alwaysApply: false --- # Create a Checklist with all Java steps to use with cursor rules for Java -You are an expert in Java programming language and technical documentation. Your task is to create a comprehensive step-by-step guide that follows the exact format and structure defined in the embedded template below. +## System prompt characterization -## Context -You have access to a set of cursor rules that java development. You need to create a structured guide that helps new users navigate through the entire set of java cursor rules. +Role definition: You are a Senior software engineer with extensive experience in Java programming language and technical documentation + +## Description + +Your task is to create a comprehensive step-by-step guide that follows the exact format and structure defined in the embedded template below. + +## Instructions for AI -## Template Structure (Self-Contained) Create a markdown file named `JAVA-DEVELOPMENT-GUIDE.md` with the following exact structure: [java-checklist-template.md](mdc:.cursor/rules/templates/java-checklist-template.md) -## CRITICAL: Strict Template Adherence -**MANDATORY REQUIREMENT**: Follow the embedded template EXACTLY - do not add, remove, or modify any steps, sections, or cursor rules that are not explicitly shown in the template. +### Restrictions + +**MANDATORY REQUIREMENT**: Follow the embedded template EXACTLY - do not add, remove, or modify any steps, sections, or cursor rules that are not explicitly shown in the template. ### What NOT to Include: -### What NOT to Include: -- **DO NOT** add framework-specific rules (Spring Boot @301, REST API @304, Quarkus @401, etc.) unless they appear in the template - **DO NOT** create additional steps beyond what's shown in the template -- **DO NOT** modify the numbering system or step structure from the template - **DO NOT** add cursor rules that are not explicitly listed in the embedded template - **DO NOT** expand or elaborate on sections beyond what the template shows - -### Template Boundaries: - **ONLY** use cursor rules that appear in the embedded template -- **ONLY** create the exact number of steps shown in the template (should be 6 steps, not more) - **ONLY** use the exact wording and structure from the template -- **ONLY** include cursor rules explicitly present in the template reference table - If a cursor rule exists in the workspace but is not in the template, **DO NOT** include it -## Instructions for AI -1. **Follow the exact format** shown in the template above -2. **Use the specific numbering system** (1.1, 1.2, etc.) as shown -3. **Include all the bash commands exactly** as specified in the template -4. **Maintain the checkbox structure** for progress tracking -5. **Keep all notes and warnings** from the original PROMPTS.md format -6. **Add the reference table and best practices** as shown in the template -7. **Make it self-contained** - no external references needed - -## Pre-Generation Validation Checklist -Before generating the guide, verify: -- [ ] All steps match the template exactly (no more, no less) -- [ ] All cursor rules included are present in the template's reference table -- [ ] No additional framework-specific rules added beyond template scope -- [ ] Step numbering system matches template structure -- [ ] Progress tracking section mirrors template format -- [ ] Tips & Best Practices section uses template content only ## Output Requirements + - Generate the complete markdown file following the embedded template exactly -- Include all sections: Prerequisites, Process Overview, Reference Table, Tips, Progress Tracking - Use proper markdown formatting with headers, code blocks, tables, and checklists -- Ensure the guide is beginner-friendly but comprehensive -- Make it portable to any repository without dependencies - **VERIFY**: Final output contains ONLY what appears in the embedded template diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..927a646a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,36 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# File generated by jbang setup@jabrena + +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.html] +indent_style = space +indent_size = 4 + +[*.json] +indent_style = space +indent_size = 4 + +[*.xml] +indent_style = space +indent_size = 4 + +[*.java] +indent_style = space +indent_size = 4 + +[*.yml,*.yaml] +indent_style = space +indent_size = 2 + +[*.dsl] +indent_style = space +indent_size = 4 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index bd9ccd5c..00000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -examples/**/*.java linguist-detectable=true -examples/**/pom.xml linguist-detectable=true diff --git a/.github/workflows/maven.yaml b/.github/workflows/maven.yaml index ab6b2fd0..d08abb3e 100644 --- a/.github/workflows/maven.yaml +++ b/.github/workflows/maven.yaml @@ -14,6 +14,8 @@ jobs: with: distribution: 'graalvm' # See 'Supported distributions' for available options java-version: '24' + - name: Generate Cursor Rules + run: cd spml && ./mvnw --batch-mode --no-transfer-progress verify --file pom.xml - name: Maven build run: cd examples/maven-demo && ./mvnw --batch-mode --no-transfer-progress verify --file pom.xml - name: Spring Boot build diff --git a/.gitignore b/.gitignore index cc4997b7..535988d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .DS_Store -tmp target/ +.idea/ +.vscode/ *.log +.classpath diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 00000000..00828075 --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1,2 @@ +java=24.0.1-graalce +maven=3.9.10 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ee8ed859..b5dffdbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.7.0] - 2025-06-30 +## [0.7.0] 2025-06-30 ### Added - **Java Profiling Support**: Added comprehensive profiling cursor rules (#81, #88, #91) - `@161-java-profiling-detect` for detecting performance issues - - `@162-java-profiling-analyze` for analyzing profiling results + - `@162-java-profiling-analyze` for analyzing profiling results - `@164-java-profiling-compare` for comparing profiling data - **Java Checklist Guide**: Added `@100-java-checklist-guide` cursor rule to help developers use cursor rules effectively (#59) - **Maven Documentation**: Added `@112-java-maven-documentation` cursor rule to generate README-DEV.md from existing pom.xml files diff --git a/README.md b/README.md index cfbddb1c..0ff47a76 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ If you have new ideas to improve any of the current Cursor rules or add a new on ## Cursor rules ecosystem - https://github.com/jabrena/101-cursor +- https://github.com/jabrena/spml - https://github.com/jabrena/cursor-rules-methodology - https://github.com/jabrena/cursor-rules-agile - https://github.com/jabrena/cursor-rules-java diff --git a/examples/quarkus-demo/README-DEV.md b/examples/quarkus-demo/README-DEV.md new file mode 100644 index 00000000..d4b3b78b --- /dev/null +++ b/examples/quarkus-demo/README-DEV.md @@ -0,0 +1,33 @@ +# Essential Maven Goals: + +```bash +# Analyze dependencies +./mvnw dependency:tree +./mvnw dependency:analyze +./mvnw dependency:resolve + +./mvnw clean validate -U +./mvnw buildplan:list-plugin +./mvnw buildplan:list-phase +./mvnw help:all-profiles +./mvnw help:active-profiles +./mvnw license:third-party-report + +# Clean the project +./mvnw clean + +# Clean and package in one command +./mvnw clean package + +# Run integration tests +./mvnw verify + +# Check for dependency updates +./mvnw versions:display-property-updates +./mvnw versions:display-dependency-updates +./mvnw versions:display-plugin-updates + +# Generate project reports +./mvnw site +jwebserver -p 8005 -d "$(pwd)/target/site/" +``` \ No newline at end of file diff --git a/spml/.mvn/jvm.config b/spml/.mvn/jvm.config new file mode 100644 index 00000000..e69de29b diff --git a/spml/.mvn/maven.config b/spml/.mvn/maven.config new file mode 100644 index 00000000..e69de29b diff --git a/spml/.mvn/wrapper/maven-wrapper.properties b/spml/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..2f94e616 --- /dev/null +++ b/spml/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.10/apache-maven-3.9.10-bin.zip diff --git a/spml/mvnw b/spml/mvnw new file mode 100755 index 00000000..19529ddf --- /dev/null +++ b/spml/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/spml/mvnw.cmd b/spml/mvnw.cmd new file mode 100644 index 00000000..b150b91e --- /dev/null +++ b/spml/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/spml/pom.xml b/spml/pom.xml new file mode 100644 index 00000000..545da77c --- /dev/null +++ b/spml/pom.xml @@ -0,0 +1,242 @@ + + + 4.0.0 + + info.jab.xml + cursor-rule-generator + 0.1.0 + + + 24 + 3.9.10 + UTF-8 + UTF-8 + + + 3.14.0 + 3.5.3 + 3.5.0 + 2.44.5 + 2.18.0 + 1.10.0 + + + 2.0.15 + 1.5.12 + + + 3.26.3 + + + + + + org.junit + junit-bom + 5.11.0 + pom + import + + + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit.jupiter + junit-jupiter-params + test + + + + org.assertj + assertj-core + ${assertj.version} + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-plugin-compiler.version} + + ${java.version} + + + + + + com.diffplug.spotless + spotless-maven-plugin + ${maven-plugin-spotless.version} + + UTF-8 + + + + ,\# + + + + + true + 4 + + + + + + + check + + process-sources + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + ${maven-plugin-enforcer.version} + + + org.codehaus.mojo + extra-enforcer-rules + ${extra-enforcer-rules.version} + + + + + enforce + + + + + + + ${maven.version} + + + ${java.version} + + + + org.projectlombok:lombok + + + + true + + + enforce + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-plugin-surefire.version} + + 1 + + **/*Test.java + + + **/*IT.java + + + + + + + org.codehaus.mojo + versions-maven-plugin + ${maven-plugin-versions.version} + + false + + + + + + org.codehaus.mojo + xml-maven-plugin + 1.1.0 + + + + validate + + validate + + + + src/main/resources + + *.xml + + true + http://www.w3.org/2001/XMLSchema + + + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + copy-cursor-rules + package + + copy-resources + + + ${project.basedir}/../.cursor/rules + + + ${project.basedir}/src/test/resources + + 100-java-checklist-guide.mdc + + + + + + + + + + diff --git a/spml/src/main/java/info/jab/xml/CursorRuleGenerator.java b/spml/src/main/java/info/jab/xml/CursorRuleGenerator.java new file mode 100644 index 00000000..dff3d6c1 --- /dev/null +++ b/spml/src/main/java/info/jab/xml/CursorRuleGenerator.java @@ -0,0 +1,179 @@ +package info.jab.xml; + +import java.io.InputStream; +import java.io.StringWriter; +import java.util.Objects; +import java.util.Optional; +import javax.xml.XMLConstants; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.*; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.XMLReader; + +/** + * Generator for Cursor Rules using XML/XSLT transformation. + * Follows functional programming principles with immutability and pure functions. + */ +public final class CursorRuleGenerator { + + private static final String XSD_FILE_NAME = "spml.xsd"; + + // =============================================================== + // PUBLIC API - Entry point for cursor rule generation + // =============================================================== + + /** + * Generates cursor rules by transforming XML with XSLT. + * Pure function that depends only on input parameters. + */ + public String generate(String xmlFileName, String xslFileName) { + return loadTransformationSources(xmlFileName, xslFileName) + .map(this::createSaxSource) + .flatMap(saxSource -> performTransformation(saxSource, xslFileName)) + .orElseThrow(() -> new RuntimeException( + "Failed to generate cursor rules for: " + xmlFileName + ", " + xslFileName)); + } + + // =============================================================== + // PRIVATE METHODS - Organized in call order for readability + // =============================================================== + + /** + * Step 1: Loads XML and XSLT resources as a TransformationSources record. + * Returns Optional to handle missing resources gracefully. + */ + private Optional loadTransformationSources(String xmlFileName, String xslFileName) { + return loadResource(xmlFileName) + .flatMap(xmlStream -> loadResource(xslFileName) + .map(xslStream -> new TransformationSources(xmlStream, xslStream))); + } + + /** + * Step 1a: Pure function to load a resource from classpath. + * Used by loadTransformationSources and performTransformation. + */ + private Optional loadResource(String fileName) { + return Optional.ofNullable( + getClass().getClassLoader().getResourceAsStream(fileName) + ); + } + + /** + * Step 2: Creates SAXSource with XSD validation. + * Pure function that creates immutable SAXSource with schema validation. + */ + private SAXSource createSaxSource(TransformationSources sources) { + try { + // Create SAX parser factory with namespace awareness and validation + SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setValidating(false); // We'll use schema validation instead + + // Load XSD schema + Optional schema = loadXsdSchema(); + if (schema.isPresent()) { + factory.setSchema(schema.get()); + } + + XMLReader xmlReader = factory.newSAXParser().getXMLReader(); + xmlReader.setErrorHandler(new ValidationErrorHandler()); + + return new SAXSource(xmlReader, new InputSource(sources.xmlStream())); + } catch (SAXException | ParserConfigurationException e) { + throw new RuntimeException("Failed to create SAX source with XSD validation", e); + } + } + + /** + * Step 3: Performs the actual XSLT transformation. + * Returns Optional to handle transformation failures gracefully. + */ + private Optional performTransformation(SAXSource xmlSource, String xslFileName) { + return loadResource(xslFileName) + .flatMap(xslStream -> executeTransformation(xmlSource, xslStream)); + } + + /** + * Step 4: Executes the transformation and returns the result. + * Encapsulates the transformation logic in a pure function. + */ + private Optional executeTransformation(SAXSource xmlSource, InputStream xslStream) { + try { + TransformerFactory factory = TransformerFactory.newInstance(); + Transformer transformer = factory.newTransformer(new StreamSource(xslStream)); + + StringWriter stringWriter = new StringWriter(); + Result result = new StreamResult(stringWriter); + + transformer.transform(xmlSource, result); + + return Optional.of(stringWriter.toString().trim()); + } catch (TransformerException e) { + // Log the exception in a real application + return Optional.empty(); + } + } + + // =============================================================== + // SUPPORTING CLASSES - Used by the main processing pipeline + // =============================================================== + + /** + * Record for holding transformation sources - immutable data transfer (internal use only). + * Used by loadTransformationSources to bundle XML and XSL streams together. + */ + private record TransformationSources(InputStream xmlStream, InputStream xslStream) { + private TransformationSources { + if (Objects.isNull(xmlStream) || Objects.isNull(xslStream)) { + throw new IllegalArgumentException("XML and XSL streams cannot be null"); + } + } + } + + /** + * Loads XSD schema from classpath for validation. + * Returns Optional to handle missing schema gracefully. + */ + private Optional loadXsdSchema() { + return loadResource(XSD_FILE_NAME) + .map(xsdStream -> { + try { + SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + return schemaFactory.newSchema(new StreamSource(xsdStream)); + } catch (SAXException e) { + throw new RuntimeException("Failed to load XSD schema: " + XSD_FILE_NAME, e); + } + }); + } + + /** + * Custom ErrorHandler for XSD validation errors. + * Provides better error reporting for validation issues. + */ + private static final class ValidationErrorHandler implements ErrorHandler { + @Override + public void warning(SAXParseException exception) throws SAXException { + // Log warning in a real application + System.err.println("XSD Validation Warning: " + exception.getMessage()); + } + + @Override + public void error(SAXParseException exception) throws SAXException { + throw new SAXException("XSD Validation Error: " + exception.getMessage(), exception); + } + + @Override + public void fatalError(SAXParseException exception) throws SAXException { + throw new SAXException("XSD Validation Fatal Error: " + exception.getMessage(), exception); + } + } +} diff --git a/spml/src/main/resources/100-java-checklist-guide.xml b/spml/src/main/resources/100-java-checklist-guide.xml new file mode 100644 index 00000000..58d49a4b --- /dev/null +++ b/spml/src/main/resources/100-java-checklist-guide.xml @@ -0,0 +1,55 @@ + + + + + + false + + java + checklist + cursor-rules + + 0.8.0 + + +
+ Create a Checklist with all Java steps to use with cursor rules for Java +
+ + + You are a Senior software engineer with extensive experience in Java programming language and technical documentation + + + + Your task is to create a comprehensive step-by-step guide that follows the exact format and structure defined in the embedded template below. + + + + + Instructions for AI + Create a markdown file named `JAVA-DEVELOPMENT-GUIDE.md` with the following exact structure: [java-checklist-template.md](mdc:.cursor/rules/templates/java-checklist-template.md) + + **MANDATORY REQUIREMENT**: Follow the embedded template EXACTLY - do not add, remove, or modify any steps, sections, or cursor rules that are not explicitly shown in the template. ### What NOT to Include: + + **DO NOT** create additional steps beyond what's shown in the template + **DO NOT** add cursor rules that are not explicitly listed in the embedded template + **DO NOT** expand or elaborate on sections beyond what the template shows + **ONLY** use cursor rules that appear in the embedded template + **ONLY** use the exact wording and structure from the template + If a cursor rule exists in the workspace but is not in the template, **DO NOT** include it + + + + + + Output Requirements + + Generate the complete markdown file following the embedded template exactly + Use proper markdown formatting with headers, code blocks, tables, and checklists + **VERIFY**: Final output contains ONLY what appears in the embedded template + + + +
diff --git a/spml/src/main/resources/110-java-maven-best-practices.xml b/spml/src/main/resources/110-java-maven-best-practices.xml new file mode 100644 index 00000000..42d09d37 --- /dev/null +++ b/spml/src/main/resources/110-java-maven-best-practices.xml @@ -0,0 +1,593 @@ + + + + Maven Best Practices + pom.xml + false + + maven + java + best-practices + + 0.8.0 + + +
+ Maven Best Practices +
+ + + You are a Senior software engineer with extensive experience in Java software development + + + + Effective Maven usage involves robust dependency management via `<dependencyManagement>` and BOMs, adherence to the standard directory layout, and centralized plugin management. Build profiles should be used for environment-specific configurations. POMs must be kept readable and maintainable with logical structure and properties for versions. Custom repositories should be declared explicitly and their use minimized, preferably managed via a central repository manager. + + + + + + + + Effective Dependency Management + Manage Dependencies Effectively using `dependencyManagement` and BOMs + + + Use the `<dependencyManagement>` section in parent POMs or import Bill of Materials (BOMs) to centralize and control dependency versions. This helps avoid version conflicts and ensures consistency across multi-module projects. Avoid hardcoding versions directly in `<dependencies>` when managed elsewhere. + + + + + + 4.0.0 + com.example + my-parent + 1.0.0 + pom + + + 5.3.23 + 5.9.0 + + + + + + org.springframework + spring-context + ${spring.version} + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + + org.springframework.boot + spring-boot-dependencies + 2.7.5 + pom + import + + + + + + + + 4.0.0 + + com.example + my-parent + 1.0.0 + + my-module + + + + org.springframework + spring-context + + + + org.junit.jupiter + junit-jupiter-api + + + +]]> + + + + + 4.0.0 + com.example + my-other-module + 1.0.0 + + + + org.springframework + spring-context + 5.3.20 + + + org.junit.jupiter + junit-jupiter-api + 5.8.1 + test + + +]]> + + + + + + + Standard Directory Layout + Adhere to the Standard Directory Layout + + + Follow Maven's convention for directory structure (`src/main/java`, `src/main/resources`, `src/test/java`, `src/test/resources`, etc.). This makes projects easier to understand and build, as Maven relies on these defaults. + + + + my-app/ +├── pom.xml +└── src/ + ├── main/ + │ ├── java/ + │ │ └── com/example/myapp/App.java + │ └── resources/ + │ └── application.properties + └── test/ + ├── java/ + │ └── com/example/myapp/AppTest.java + └── resources/ + └── test-data.xml + + + my-app/ +├── pom.xml +├── sources/ <!-- Non-standard --> +│ └── com/example/myapp/App.java +├── res/ <!-- Non-standard --> +│ └── config.properties +└── tests/ <!-- Non-standard --> + └── com/example/myapp/AppTest.java +<!-- This would require explicit configuration in pom.xml to specify source/resource directories --> + + + + + + + Plugin Management and Configuration + Manage Plugin Versions and Configurations Centrally + + + Use `<pluginManagement>` in a parent POM to define plugin versions and common configurations. Child POMs can then use the plugins without specifying versions, ensuring consistency. Override configurations in child POMs only when necessary. + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.1 + + 17 + 17 + + + + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + +]]> + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 11 + 11 + + + + +]]> + + + + + + + Use Build Profiles for Environment-Specific Configurations + Employ Build Profiles for Environment-Specific Settings + + + Use Maven profiles to customize build settings for different environments (e.g., dev, test, prod) or other conditional scenarios. This can include different dependencies, plugin configurations, or properties. Activate profiles via command line, OS, JDK, or file presence. + + + + + + + + dev + + true + + + jdbc:h2:mem:devdb + + + + prod + + jdbc:postgresql://prodserver/mydb + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + package + run + + + + Simulating minification for prod + + + + + + + + + + + + ]]> + + + + + + + + jdbc:postgresql://prodserver/mydb + +]]> + + + + + + + Keep POMs Readable and Maintainable + Structure POMs Logically for Readability + + + Organize your `pom.xml` sections in a consistent order (e.g., project coordinates, parent, properties, dependencyManagement, dependencies, build, profiles, repositories). Use properties for recurring versions or values. Add comments for complex configurations. + + + + + 4.0.0 + + + com.example + my-app + 1.0.0-SNAPSHOT + jar + My Application + A sample application. + + + + + + + 17 + UTF-8 + 2.5.1 + + + + + + + + + + + org.some.library + some-library-core + ${some.library.version} + + + + + + + + + + + + + + +]]> + + + + + + + org.some.library + some-library-core + 2.5.1 + + + 4.0.0 + + + + com.example + + UTF-8 + + my-app + 1.0.0-SNAPSHOT +]]> + + + + + + + Manage Repositories Explicitly + Declare Custom Repositories Explicitly and Minimize Their Use + + + Prefer dependencies from Maven Central. If custom repositories are necessary, declare them in the `<repositories>` section and `<pluginRepositories>` for plugins. It's often better to manage these in a company-wide Nexus/Artifactory instance configured in `settings.xml` rather than per-project POMs. Avoid relying on transitive repositories. + + + + + + + + my-internal-repo + https://nexus.example.com/repository/maven-releases/ + + + + + my-internal-plugins + https://nexus.example.com/repository/maven-plugins/ + + + +]]> + + + + + + + + com.internal.stuff + internal-lib + 1.0 + + + +]]> + + + + + + + Centralize Version Management with Properties + Use Properties to Manage Dependency and Plugin Versions + + + Define all dependency and plugin versions in the `<properties>` section rather than hardcoding them throughout the POM. This centralizes version management, makes updates easier, reduces duplication, and helps maintain consistency across related dependencies. Use consistent property naming conventions: `maven-plugin-[name].version` for Maven plugins, simple names like `[library].version` for dependencies, and descriptive names for quality thresholds like `coverage.level`. + + + + + 4.0.0 + com.example + my-app + 1.0.0 + + + + 17 + 3.9.10 + UTF-8 + UTF-8 + + + 2.15.3 + 5.10.1 + 5.7.0 + 1.4.11 + + + 3.14.0 + 3.5.3 + 3.5.3 + 3.5.0 + + + 0.8.13 + + + 80 + 70 + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-plugin-compiler.version} + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-plugin-surefire.version} + + + org.jacoco + jacoco-maven-plugin + ${maven-plugin-jacoco.version} + + + +]]> + + + + + 4.0.0 + com.example + my-app + 1.0.0 + + + 17 + UTF-8 + + + + + com.fasterxml.jackson.core + jackson-databind + 2.15.3 + + + com.fasterxml.jackson.core + jackson-core + 2.15.2 + + + org.junit.jupiter + junit-jupiter-api + 5.10.1 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.9.3 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.2 + + + org.jacoco + jacoco-maven-plugin + 0.8.10 + + + +]]> + + + + +
diff --git a/spml/src/main/resources/112-java-maven-documentation.xml b/spml/src/main/resources/112-java-maven-documentation.xml new file mode 100644 index 00000000..e0fff6f1 --- /dev/null +++ b/spml/src/main/resources/112-java-maven-documentation.xml @@ -0,0 +1,78 @@ + + + + Create README-DEV.md with information about how to use the Maven project + pom.xml + false + + maven + java + documentation + + 0.8.0 + + +
+ Create README-DEV.md with information about how to use the Maven project +
+ + + You are a Senior software engineer with extensive experience in Java software development + + + + When creating a README-DEV.md file for a Maven project, include ONLY the following sections with the specified Maven goals. Do NOT add any additional sections, explanations, or content beyond what is explicitly listed below. + + + + + + STRICT Structure for README-DEV.md (Template) + + + **IMPORTANT: Include ONLY the content specified below.** + + + +# Essential Maven Goals: + +```bash +# Analyze dependencies +./mvnw dependency:tree +./mvnw dependency:analyze +./mvnw dependency:resolve + +./mvnw clean validate -U +./mvnw buildplan:list-plugin +./mvnw buildplan:list-phase +./mvnw help:all-profiles +./mvnw help:active-profiles +./mvnw license:third-party-report + +# Clean the project +./mvnw clean + +# Clean and package in one command +./mvnw clean package + +# Run integration tests +./mvnw verify + +# Check for dependency updates +./mvnw versions:display-property-updates +./mvnw versions:display-dependency-updates +./mvnw versions:display-plugin-updates + +# Generate project reports +./mvnw site +jwebserver -p 8005 -d "$(pwd)/target/site/" +``` + +**END OF TEMPLATE - DO NOT ADD ANYTHING BEYOND THIS POINT** + + + + +
diff --git a/spml/src/main/resources/121-java-object-oriented-design.xml b/spml/src/main/resources/121-java-object-oriented-design.xml new file mode 100644 index 00000000..0fb8fb72 --- /dev/null +++ b/spml/src/main/resources/121-java-object-oriented-design.xml @@ -0,0 +1,310 @@ + + + + Java Object-Oriented Design Guidelines + *.java + false + + java + object-oriented + design + best-practices + + 0.8.0 + + +
+ Java Object-Oriented Design Guidelines +
+ + + You are a Senior software engineer with extensive experience in Java software development + + + + This document provides comprehensive guidelines for robust Java object-oriented design and refactoring. It emphasizes core principles like SOLID, DRY, and YAGNI, best practices for class and interface design including favoring composition over inheritance and designing for immutability. The rules also cover mastering encapsulation, inheritance, and polymorphism, and finally, identifying and refactoring common object-oriented design code smells such as God Classes, Feature Envy, and Data Clumps to promote maintainable, flexible, and understandable code. + + + + + + + + Adhere to Core Design Principles (SOLID, DRY, YAGNI) + Apply Fundamental Software Design Principles + + + Core principles like SOLID, DRY, and YAGNI are foundational to good object-oriented design, leading to more robust, maintainable, and understandable systems. These principles should guide all design decisions and help create systems that are flexible and resistant to change. + + + + + + + + + + + + + + Follow Best Practices for Class and Interface Design + Design Well-Structured and Maintainable Classes and Interfaces + + + Good class and interface design is crucial for building flexible and understandable OOD systems. Favor composition over inheritance, program to interfaces rather than implementations, keep classes small and focused, and design for immutability where appropriate. Use clear, descriptive naming conventions. + + + + + + + + + + + + + + Master Encapsulation, Inheritance, and Polymorphism + Effectively Utilize Core Object-Oriented Concepts + + + Encapsulation, Inheritance, and Polymorphism are the three pillars of object-oriented programming. Proper encapsulation protects internal state and exposes behavior through well-defined interfaces. Inheritance should model true "is-a" relationships following the Liskov Substitution Principle. Polymorphism allows objects of different types to be treated uniformly. + + + + + + + + + + + + + + Identify and Refactor Object-Oriented Design Code Smells + Recognize and Fix Common Design Anti-Patterns + + + Develop the ability to identify common object-oriented design "code smells" such as God Class, Feature Envy, Data Clumps, and Refused Bequest. Recognizing and refactoring these smells is crucial for improving the long-term health, maintainability, and clarity of the codebase. + + + + items; + private final Customer customer; + + public Order(String orderId, Customer customer) { + this.orderId = orderId; + this.customer = customer; + this.items = new ArrayList<>(); + } + + public void addItem(OrderItem item) { items.add(item); } + public List getItems() { return Collections.unmodifiableList(items); } + public Customer getCustomer() { return customer; } + public String getOrderId() { return orderId; } +} + +class OrderCalculator { + public BigDecimal calculateTotal(Order order) { + return order.getItems().stream() + .map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } +} + +class OrderValidator { + public boolean isValid(Order order) { + return !order.getItems().isEmpty() && + order.getCustomer() != null && + order.getCustomer().hasValidPaymentMethod(); + } +}]]> + + + items; + private Customer customer; + private PaymentProcessor paymentProcessor; + private InventoryService inventoryService; + private EmailService emailService; + private TaxCalculator taxCalculator; + + // Dozens of methods doing various unrelated things: + public void addItem(OrderItem item) { /* ... */ } + public void removeItem(String itemId) { /* ... */ } + public BigDecimal calculateSubtotal() { /* ... */ } + public BigDecimal calculateTax() { /* ... */ } + public BigDecimal calculateShipping() { /* ... */ } + public boolean validateInventory() { /* ... */ } + public void processPayment() { /* ... */ } + public void sendConfirmationEmail() { /* ... */ } + public void updateInventory() { /* ... */ } + public void generateInvoice() { /* ... */ } + public void trackShipment() { /* ... */ } + // ... many more methods + + // This class has too many responsibilities and reasons to change +}]]> + + + + +
diff --git a/spml/src/main/resources/122-java-type-design.xml b/spml/src/main/resources/122-java-type-design.xml new file mode 100644 index 00000000..bb936784 --- /dev/null +++ b/spml/src/main/resources/122-java-type-design.xml @@ -0,0 +1,499 @@ + + + + Type Design Thinking in Java + *.java + false + + java + type-design + naming + generics + best-practices + + 0.8.0 + + +
+ Type Design Thinking in Java +
+ + + You are a Senior software engineer with extensive experience in Java software development + + + + Type design thinking in Java applies typography principles to code structure and organization. Just as typography creates readable, accessible text, thoughtful type design in Java produces maintainable, comprehensible code. This document focuses on establishing clear type hierarchies, using consistent naming conventions, leveraging generics effectively, and creating type-safe wrappers that communicate intent clearly. + + + + + + + + Establish a Clear Type Hierarchy + Organize Classes and Interfaces into Logical Structure + + + This rule focuses on organizing classes and interfaces into a logical structure using inheritance and composition. A clear hierarchy makes the relationships between types explicit, improving code navigation and understanding. It often involves using nested static classes for closely related types. + + + + items; + private Customer customer; + private OrderStatus status; + + public Order(Customer customer) { + this.customer = customer; + this.items = new ArrayList<>(); + this.status = OrderStatus.PENDING; + } + + public void addItem(OrderItem item) { items.add(item); } + public List getItems() { return Collections.unmodifiableList(items); } + public Customer getCustomer() { return customer; } + public OrderStatus getStatus() { return status; } + } + + public static class OrderItem { + private Product product; + private int quantity; + private BigDecimal unitPrice; + + public OrderItem(Product product, int quantity, BigDecimal unitPrice) { + this.product = product; + this.quantity = quantity; + this.unitPrice = unitPrice; + } + + public Product getProduct() { return product; } + public int getQuantity() { return quantity; } + public BigDecimal getUnitPrice() { return unitPrice; } + public BigDecimal getTotalPrice() { return unitPrice.multiply(BigDecimal.valueOf(quantity)); } + } + + public enum OrderStatus { + PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED + } +}]]> + + + items; // What kind of item? + private User user; // Is this a customer, admin, or something else? + private int status; // What do the numbers mean? + // ... +} + +public class Item { // Too generic - what kind of item? + private Thing thing; // What is a "thing"? + private int count; + // ... +} + +public class User { // Too generic - could be any type of user + private String data; // What kind of data? + // ... +}]]> + + + + + + + Use Consistent Naming Conventions + Apply Uniform Patterns for Naming (Your Type's "Font Family") + + + This rule emphasizes using uniform patterns for naming classes, interfaces, methods, and variables. Consistency in naming acts like a consistent font family in typography, making the code easier to read, predict, and maintain across the entire project. + + + + + + + + + + + + + + Create Type-Safe Wrappers + Use Types as Communication Tools + + + This rule encourages wrapping primitive types or general-purpose types (like String) in domain-specific types. These wrapper types enhance type safety by enforcing invariants at compile-time and clearly communicate the intended meaning and constraints of data. + + + + + + + + + + + + + + Leverage Generic Type Parameters + Create Flexible and Reusable Types (Responsive Typography) + + + This rule promotes the use of generics to create flexible and reusable types and methods that can operate on objects of various types while maintaining type safety. This is akin to responsive typography that adapts to different screen sizes, as generics adapt to different data types. + + + + { + private final Class entityClass; + private final EntityManager entityManager; + + public Repository(Class entityClass, EntityManager entityManager) { + this.entityClass = entityClass; + this.entityManager = entityManager; + } + + public Optional findById(Long id) { + T entity = entityManager.find(entityClass, id); + return Optional.ofNullable(entity); + } + + public List findAll() { + CriteriaQuery query = entityManager.getCriteriaBuilder() + .createQuery(entityClass); + query.select(query.from(entityClass)); + return entityManager.createQuery(query).getResultList(); + } + + public T save(T entity) { + if (entity.getId() == null) { + entityManager.persist(entity); + return entity; + } else { + return entityManager.merge(entity); + } + } + + public void delete(T entity) { + entityManager.remove(entity); + } +} + +// Usage for different entity types with full type safety +Repository customerRepo = new Repository<>(Customer.class, em); +Repository productRepo = new Repository<>(Product.class, em); + +// Type-safe operations +Optional customer = customerRepo.findById(1L); +List products = productRepo.findAll();]]> + + + findById(Long id) { + Customer customer = entityManager.find(Customer.class, id); + return Optional.ofNullable(customer); + } + + public List findAll() { + // Duplicated logic + CriteriaQuery query = entityManager.getCriteriaBuilder() + .createQuery(Customer.class); + query.select(query.from(Customer.class)); + return entityManager.createQuery(query).getResultList(); + } + + public Customer save(Customer customer) { + // Duplicated logic + if (customer.getId() == null) { + entityManager.persist(customer); + return customer; + } else { + return entityManager.merge(customer); + } + } +} + +public class ProductRepository { + // Exact same code but for Product - massive duplication! + private final EntityManager entityManager; + + public ProductRepository(EntityManager entityManager) { + this.entityManager = entityManager; + } + + public Optional findById(Long id) { + Product product = entityManager.find(Product.class, id); + return Optional.ofNullable(product); + } + + // ... more duplicated methods +}]]> + + + + + + + Use BigDecimal for Precision-Sensitive Calculations + Ensure Accuracy in Financial and Mathematical Operations + + + This rule emphasizes using `java.math.BigDecimal` for calculations requiring high precision, especially with monetary values or any domain where rounding errors from binary floating-point arithmetic (like `float` or `double`) are unacceptable. Use consistent rounding modes and scale for predictable results. + + + + + + + + + + + +
diff --git a/spml/src/main/resources/123-java-general-guidelines.xml b/spml/src/main/resources/123-java-general-guidelines.xml new file mode 100644 index 00000000..bcec158c --- /dev/null +++ b/spml/src/main/resources/123-java-general-guidelines.xml @@ -0,0 +1,337 @@ + + + + Java General Guidelines + *.java + false + + java + guidelines + naming + formatting + documentation + exceptions + best-practices + + 1.0.0 + + +
+ Java General Guidelines +
+ + + You are a Senior software engineer with extensive experience in Java software development + + + + This document outlines general Java coding guidelines covering fundamental aspects such as naming conventions for packages, classes, methods, variables, and constants; code formatting rules including indentation, line length, brace style, and whitespace usage; standards for organizing import statements; best practices for Javadoc documentation; and comprehensive error and exception handling with a strong focus on security, including avoiding sensitive information exposure, catching specific exceptions, and secure resource management. + + + + + + + + Naming Conventions + Follow Standard Java Naming Patterns + + + Adhere to standard Java naming conventions for all code elements to promote intuitive, predictable, and easier to understand code navigation. + + + + { // Single uppercase letter + // ... implementation +}]]> + + + + + + + + + + Formatting + Apply Consistent Code Formatting + + + Consistently apply formatting rules for indentation, line length, brace style, and whitespace to improve code readability and maintainability. + + + + + + + + + + + + + + Import Statements + Organize Imports Systematically + + + Structure import statements logically by grouping related packages and alphabetizing within groups. Avoid wildcard imports to ensure clarity about class origins. + + + + + + + + + + + + + + Documentation Standards + Maintain Clear Documentation + + + Write self-documenting code and provide comprehensive Javadoc for public APIs, complex algorithms, and non-obvious business logic with required elements like @param, @return, @throws. + + + + \"'&]", ""); + + return sanitized.trim(); + } +}]]> + + + ]", ""); + } +}]]> + + + + + + + Comprehensive Error and Exception Handling + Implement Secure and Robust Error Management + + + Implement robust error handling using specific exceptions, managing them at appropriate levels while preventing information leakage and ensuring proper resource cleanup. + + + + + + + + + + + +
diff --git a/spml/src/main/resources/124-java-secure-coding.xml b/spml/src/main/resources/124-java-secure-coding.xml new file mode 100644 index 00000000..91a183f0 --- /dev/null +++ b/spml/src/main/resources/124-java-secure-coding.xml @@ -0,0 +1,652 @@ + + + + Java Secure coding guidelines + *.java + false + + java + security + validation + injection + cryptography + exceptions + secure-practices + + 1.0.0 + + +
+ Java Secure coding guidelines +
+ + + You are a Senior software engineer with extensive experience in Java software development + + + + This document provides essential Java secure coding guidelines, focusing on five key areas: validating all untrusted inputs to prevent attacks like injection and path traversal; protecting against injection attacks (e.g., SQL injection) by using parameterized queries or prepared statements; minimizing the attack surface by adhering to the principle of least privilege and reducing exposure; employing strong, current cryptographic algorithms for hashing, encryption, and digital signatures while avoiding deprecated ones; and handling exceptions securely by avoiding the exposure of sensitive information in error messages to users and logging detailed, non-sensitive diagnostic information for developers. + + + + + + + + Input Validation + Validate All Untrusted Inputs + + + Always validate and sanitize data received from untrusted sources (users, network, files, etc.) before processing. This helps prevent various attacks like injection, path traversal, and buffer overflows. Validation should check for type, length, format, and range. + + + + MAX_AGE) { + throw new IllegalArgumentException("Age must be between " + MIN_AGE + " and " + MAX_AGE); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid age format. Must be a valid integer.", e); + } + + // Input is now validated and safe to process + System.out.println("Processing validated user: " + username + ", age: " + age); + } + + public String sanitizeFilePath(String userPath) { + if (userPath == null) { + throw new IllegalArgumentException("File path cannot be null"); + } + + // Prevent path traversal attacks + String sanitized = userPath.replaceAll("\\.\\.", "").replaceAll("/", ""); + + // Additional validation + if (sanitized.length() > 255) { + throw new IllegalArgumentException("File path too long"); + } + + return sanitized; + } +}]]> + + + + + + + + + + Protect Against Injection Attacks + Use Parameterized Queries and Safe APIs + + + To prevent SQL Injection and other injection attacks, always use parameterized queries (PreparedStatements in JDBC) or an ORM that handles this automatically. Never concatenate user input directly into SQL queries, OS commands, or other executable statements. + + + + getOrdersByCustomerId(String customerId) throws SQLException { + // Safe parameterized query + String query = "SELECT order_id, customer_id, amount FROM orders WHERE customer_id = ?"; + List orders = new ArrayList<>(); + + try (Connection con = DriverManager.getConnection(DB_URL, USER, PASS); + PreparedStatement pstmt = con.prepareStatement(query)) { + + pstmt.setString(1, customerId); // Parameter is safely bound + ResultSet rs = pstmt.executeQuery(); + + while (rs.next()) { + Order order = new Order(); + order.setOrderId(rs.getString("order_id")); + order.setCustomerId(rs.getString("customer_id")); + order.setAmount(rs.getBigDecimal("amount")); + orders.add(order); + } + } + + return orders; + } + + public void updateCustomerEmail(String customerId, String newEmail) throws SQLException { + String updateQuery = "UPDATE customers SET email = ? WHERE customer_id = ?"; + + try (Connection con = DriverManager.getConnection(DB_URL, USER, PASS); + PreparedStatement pstmt = con.prepareStatement(updateQuery)) { + + pstmt.setString(1, newEmail); + pstmt.setString(2, customerId); + pstmt.executeUpdate(); + } + } +}]]> + + + getOrdersByCustomerId(String customerId) throws SQLException { + // DANGEROUS: User input directly concatenated into SQL query + String query = "SELECT order_id, customer_id, amount FROM orders WHERE customer_id = '" + customerId + "'"; + List orders = new ArrayList<>(); + + try (Connection con = DriverManager.getConnection(DB_URL, USER, PASS); + Statement stmt = con.createStatement()) { + + // User could pass: "'; DROP TABLE orders; --" to execute malicious SQL + ResultSet rs = stmt.executeQuery(query); + + while (rs.next()) { + // Process results... + } + } + + return orders; + } + + public void executeCommand(String userCommand) { + // EXTREMELY DANGEROUS: Command injection vulnerability + String command = "ls " + userCommand; // User could inject "; rm -rf /" + try { + Runtime.getRuntime().exec(command); + } catch (Exception e) { + // Handle exception + } + } +}]]> + + + + + + + Minimize Attack Surface + Apply Principle of Least Privilege + + + Grant only necessary permissions to code and users. Avoid running processes with excessive privileges. Expose only essential functionality and network ports. Regularly review and remove unused features, libraries, and accounts. + + + + ALLOWED_EXTENSIONS = Set.of(".txt", ".log", ".json"); + private static final Path SAFE_DIRECTORY = Paths.get("/app/data/uploads"); + + public String readUserFile(String filename, Principal user) throws SecurityException, IOException { + // Check user permissions + if (!hasReadPermission(user, filename)) { + throw new SecurityException("User does not have permission to read this file"); + } + + // Validate file extension + String extension = getFileExtension(filename); + if (!ALLOWED_EXTENSIONS.contains(extension)) { + throw new SecurityException("File type not allowed: " + extension); + } + + // Ensure file is within safe directory + Path filePath = SAFE_DIRECTORY.resolve(filename).normalize(); + if (!filePath.startsWith(SAFE_DIRECTORY)) { + throw new SecurityException("File access outside allowed directory"); + } + + // Check file size limits + if (Files.size(filePath) > 1024 * 1024) { // 1MB limit + throw new SecurityException("File too large"); + } + + return Files.readString(filePath); + } + + private boolean hasReadPermission(Principal user, String filename) { + // Implement proper authorization logic + return user != null && user.getName() != null; + } + + private String getFileExtension(String filename) { + int lastDot = filename.lastIndexOf('.'); + return lastDot > 0 ? filename.substring(lastDot) : ""; + } +} + +// Example of interface segregation - expose only necessary methods +public interface UserService { + User findById(Long id); + void updateProfile(Long id, UserProfile profile); + // Don't expose administrative methods to regular users +} + +public interface AdminService extends UserService { + void deleteUser(Long id); + List getAllUsers(); + void resetPassword(Long id); +}]]> + + + + + + + + + + Use Strong Cryptography + Employ Current and Robust Cryptographic Algorithms + + + Use well-vetted, industry-standard cryptographic libraries and algorithms for hashing, encryption, and digital signatures. Avoid deprecated or weak algorithms (e.g., MD5, SHA1 for passwords, DES). Keep cryptographic keys secure and manage them properly. + + + + + + + + + + + + + + Handle Exceptions Securely + Avoid Exposing Sensitive Information + + + Catch exceptions appropriately, but do not reveal sensitive system details or stack traces to users in production. Log detailed error information server-side for debugging, but provide generic error messages to the client. + + + + + + + + + + + +
diff --git a/spml/src/main/resources/125-java-concurrency.xml b/spml/src/main/resources/125-java-concurrency.xml new file mode 100644 index 00000000..d118d804 --- /dev/null +++ b/spml/src/main/resources/125-java-concurrency.xml @@ -0,0 +1,892 @@ + + + + Java rules for Concurrency objects + *.java + false + + java + concurrency + threads + executors + futures + synchronization + performance + + 1.0.0 + + +
+ Java rules for Concurrency objects +
+ + + You are a Senior software engineer with extensive experience in Java software development + + + + Effective Java concurrency relies on understanding thread safety fundamentals, using `java.util.concurrent` utilities, and managing thread pools with `ExecutorService`. Key practices include implementing concurrent design patterns like Producer-Consumer, leveraging `CompletableFuture` for asynchronous tasks, and ensuring thread safety through immutability and safe publication. Performance aspects like lock contention and memory consistency must be considered. Thorough testing, including stress tests and thread dump analysis, is crucial. Modern Java offers virtual threads for enhanced scalability, structured concurrency for simplified task management, and scoped values for safer thread-shared data as alternatives to thread-locals. + + + + + + + + Thread Safety Fundamentals + Master Core Thread Safety Concepts + + + Understand and correctly apply core concepts such as synchronization, atomic operations, thread-safe collections, immutability, and the Java Memory Model to ensure data integrity and prevent race conditions or deadlocks. + + + + concurrentMap = new ConcurrentHashMap<>(); + private final Queue taskQueue = new ConcurrentLinkedQueue<>(); + private final BlockingQueue eventQueue = new LinkedBlockingQueue<>(); + + // Atomic variables for lock-free operations + private final AtomicInteger counter = new AtomicInteger(0); + private final AtomicReference status = new AtomicReference<>("INIT"); + + // Thread-local storage + private static final ThreadLocal userContext = + ThreadLocal.withInitial(() -> "default"); + + // Using ReentrantLock for complex synchronization + private final ReentrantLock lock = new ReentrantLock(); + private int sharedResource = 0; + + public void incrementCounter() { + // Atomic operation - thread-safe without locks + int newValue = counter.incrementAndGet(); + concurrentMap.put("lastCount", String.valueOf(newValue)); + } + + public void updateSharedResource(int delta) { + lock.lock(); + try { + sharedResource += delta; + System.out.println("Updated resource: " + sharedResource); + } finally { + lock.unlock(); // Always unlock in finally block + } + } + + // Using ReadWriteLock for better performance with frequent reads + private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); + private String sharedData = "Initial Data"; + + public String readData() { + rwLock.readLock().lock(); + try { + return sharedData; + } finally { + rwLock.readLock().unlock(); + } + } + + public void writeData(String data) { + rwLock.writeLock().lock(); + try { + sharedData = data; + } finally { + rwLock.writeLock().unlock(); + } + } +}]]> + + + unsafeMap = new HashMap<>(); + private List unsafeList = new ArrayList<>(); + + // BAD: Using plain int without synchronization + private int counter = 0; + private String status = "INIT"; + + public void incrementCounter() { + // RACE CONDITION: Multiple threads can read same value + counter++; // Not atomic - can lose updates + unsafeMap.put("lastCount", String.valueOf(counter)); // Can corrupt map + } + + // BAD: Inconsistent synchronization + public synchronized void updateCounter(int value) { + counter = value; // Synchronized + } + + public int getCounter() { + return counter; // NOT synchronized - can read stale value + } + + // BAD: Synchronizing on mutable object + private String lockObject = "lock"; + + public void badSynchronization() { + synchronized (lockObject) { // WRONG: string can be changed + // Critical section + } + lockObject = "newLock"; // Now synchronization is broken! + } +}]]> + + + + + + + Thread Pool Management + Manage Thread Pools Effectively with ExecutorService + + + Utilize ExecutorService for robust thread management. Choose appropriate thread pool implementations, configure them properly, and implement graceful shutdown procedures. + + + + (100), // bounded queue + new CustomThreadFactory("custom"), + new ThreadPoolExecutor.CallerRunsPolicy() // rejection policy + ); + } + + public void submitTask(Runnable task) { + fixedPool.submit(task); + } + + public void schedulePeriodicTask(Runnable task, long period) { + scheduler.scheduleAtFixedRate(task, 0, period, TimeUnit.SECONDS); + } + + public void shutdown() { + shutdownExecutorService(fixedPool, "FixedPool"); + shutdownExecutorService(scheduler, "Scheduler"); + shutdownExecutorService(customPool, "CustomPool"); + } + + private void shutdownExecutorService(ExecutorService executor, String name) { + executor.shutdown(); // Disable new tasks + try { + // Wait for existing tasks to terminate + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { + executor.shutdownNow(); // Cancel currently executing tasks + + // Wait for tasks to respond to being cancelled + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { + System.err.println("Pool " + name + " did not terminate"); + } + } + } catch (InterruptedException ie) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + // Custom thread factory for better thread naming and error handling + private static class CustomThreadFactory implements ThreadFactory { + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + CustomThreadFactory(String namePrefix) { + this.namePrefix = namePrefix + "-thread-"; + } + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement()); + t.setDaemon(false); + t.setUncaughtExceptionHandler((thread, ex) -> { + System.err.println("Thread " + thread.getName() + " threw exception: " + ex); + }); + return t; + } + } +}]]> + + + { + try { + Thread.sleep(10000); // Long-running task + } catch (InterruptedException e) { + // BAD: Ignoring interruption + } + }); + } + } + + // BAD: No proper shutdown + public void shutdown() { + cachedPool.shutdown(); // What if tasks don't finish? + singleThread.shutdown(); + // No waiting for termination + // No handling of interrupted exception + // No forced shutdown if graceful shutdown fails + } + + // BAD: Creating new thread for each task + public void executeTask(Runnable task) { + new Thread(task).start(); // Expensive and uncontrolled + } + + // BAD: No thread naming or error handling + // Default thread names are not descriptive + // Uncaught exceptions terminate threads silently +}]]> + + + + + + + Concurrent Design Patterns + Implement Producer-Consumer and Publish-Subscribe + + + Leverage established patterns like Producer-Consumer and Publish-Subscribe to structure concurrent applications effectively, promoting decoupling, scalability, and maintainability. + + + + queue = new LinkedBlockingQueue<>(100); + private final ExecutorService executor = Executors.newFixedThreadPool(4); + private volatile boolean running = true; + + public void startProcessing() { + // Start multiple consumers + for (int i = 0; i < 2; i++) { + executor.submit(this::consumer); + } + } + + public void produce(Task task) throws InterruptedException { + if (running) { + queue.put(task); // Blocks if queue is full + } + } + + private void consumer() { + while (running || !queue.isEmpty()) { + try { + Task task = queue.poll(1, TimeUnit.SECONDS); + if (task != null) { + processTask(task); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + + private void processTask(Task task) { + System.out.println("Processing: " + task + " on " + Thread.currentThread().getName()); + // Simulate work + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public void shutdown() { + running = false; + executor.shutdown(); + } + + private static class Task { + private final String data; + public Task(String data) { this.data = data; } + @Override public String toString() { return "Task(" + data + ")"; } + } +} + +// Publish-Subscribe implementation +public class EventBus { + private final ConcurrentHashMap> listeners = new ConcurrentHashMap<>(); + private final ExecutorService notificationExecutor = ForkJoinPool.commonPool(); + + public void subscribe(String topic, EventListener listener) { + listeners.computeIfAbsent(topic, k -> ConcurrentHashMap.newKeySet()) + .add(listener); + } + + public void unsubscribe(String topic, EventListener listener) { + Set topicListeners = listeners.get(topic); + if (topicListeners != null) { + topicListeners.remove(listener); + } + } + + public void publish(String topic, Event event) { + Set topicListeners = listeners.get(topic); + if (topicListeners != null && !topicListeners.isEmpty()) { + // Notify listeners asynchronously + topicListeners.forEach(listener -> + notificationExecutor.submit(() -> { + try { + listener.onEvent(event); + } catch (Exception e) { + System.err.println("Error notifying listener: " + e.getMessage()); + } + }) + ); + } + } + + private static class Event { + private final String data; + public Event(String data) { this.data = data; } + @Override public String toString() { return "Event(" + data + ")"; } + } + + @FunctionalInterface + private interface EventListener { + void onEvent(Event event); + } +}]]> + + + tasks = new ArrayList<>(); + private boolean running = true; + + public void produce(String task) { + // RACE CONDITION: Multiple producers can corrupt the list + synchronized (this) { + tasks.add(task); + notify(); // BAD: Should use notifyAll() + } + } + + public void consume() { + while (running) { + String task = null; + synchronized (this) { + while (tasks.isEmpty() && running) { + try { + wait(); // BAD: Can miss notifications + } catch (InterruptedException e) { + // BAD: Not handling interruption properly + return; + } + } + if (!tasks.isEmpty()) { + task = tasks.remove(0); // BAD: Inefficient removal from front + } + } + + if (task != null) { + // BAD: Processing inside synchronized block would be even worse + processTask(task); + } + } + } + + // BAD: No proper shutdown mechanism + public void stop() { + running = false; // Consumers might not wake up + } +} + +// BAD: Synchronous event handling +public class BadEventBus { + private Map> listeners = new HashMap<>(); + + public void subscribe(String topic, EventListener listener) { + // BAD: Not thread-safe + listeners.computeIfAbsent(topic, k -> new ArrayList<>()).add(listener); + } + + public void publish(String topic, String event) { + List topicListeners = listeners.get(topic); + if (topicListeners != null) { + // BAD: Synchronous notification blocks publisher + for (EventListener listener : topicListeners) { + try { + listener.onEvent(event); + } catch (Exception e) { + // BAD: One failing listener affects others + throw new RuntimeException("Event handling failed", e); + } + } + } + } +}]]> + + + + + + + Asynchronous Programming with CompletableFuture + Compose Non-blocking Asynchronous Operations + + + Employ CompletableFuture to compose and manage asynchronous computations in a non-blocking way. Chain dependent tasks, combine results from multiple futures, and handle exceptions gracefully. + + + + processDataAsync(String input) { + return CompletableFuture + .supplyAsync(() -> validateInput(input), customExecutor) + .thenApplyAsync(this::transformData, customExecutor) + .thenApply(this::formatResult) + .exceptionally(this::handleError) + .whenComplete((result, ex) -> { + if (ex != null) { + System.err.println("Processing failed for: " + input); + } else { + System.out.println("Successfully processed: " + input); + } + }); + } + + public CompletableFuture> processMultipleAsync(List inputs) { + List> futures = inputs.stream() + .map(this::processDataAsync) + .collect(Collectors.toList()); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList())) + .exceptionally(ex -> { + System.err.println("Batch processing failed: " + ex.getMessage()); + return List.of("ERROR"); + }); + } + + public CompletableFuture combineResults(String input1, String input2) { + CompletableFuture future1 = processDataAsync(input1); + CompletableFuture future2 = processDataAsync(input2); + + return future1.thenCombine(future2, (result1, result2) -> + "Combined: " + result1 + " + " + result2); + } + + public CompletableFuture getFirstSuccessful(List inputs) { + CompletableFuture[] futures = inputs.stream() + .map(this::processDataAsync) + .toArray(CompletableFuture[]::new); + + return CompletableFuture.anyOf(futures) + .thenApply(result -> (String) result); + } + + private String validateInput(String input) { + if (input == null || input.trim().isEmpty()) { + throw new IllegalArgumentException("Input cannot be null or empty"); + } + // Simulate validation work + try { Thread.sleep(100); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + return input.trim(); + } + + private String transformData(String input) { + // Simulate transformation work + try { Thread.sleep(200); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + return "transformed_" + input; + } + + private String formatResult(String input) { + return "[" + input + "]"; + } + + private String handleError(Throwable throwable) { + System.err.println("Error occurred: " + throwable.getMessage()); + return "ERROR: " + throwable.getClass().getSimpleName(); + } + + public void shutdown() { + customExecutor.shutdown(); + try { + if (!customExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + customExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + customExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } +}]]> + + + future = CompletableFuture.supplyAsync(() -> { + return processInput(input); + }); + + try { + return future.get(); // BLOCKING! Defeats the purpose + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public List processMultipleBlocking(List inputs) { + List results = new ArrayList<>(); + + // BAD: Sequential processing instead of parallel + for (String input : inputs) { + CompletableFuture future = CompletableFuture.supplyAsync(() -> + processInput(input)); + + try { + results.add(future.get()); // BLOCKING in loop + } catch (Exception e) { + // BAD: One failure stops everything + throw new RuntimeException("Processing failed", e); + } + } + + return results; + } + + public CompletableFuture badErrorHandling(String input) { + return CompletableFuture.supplyAsync(() -> { + if ("fail".equals(input)) { + throw new RuntimeException("Simulated failure"); + } + return processInput(input); + }); + // BAD: No error handling - exceptions will propagate + } + + public void badChaining(String input) { + // BAD: Not chaining properly, creating nested futures + CompletableFuture> nestedFuture = + CompletableFuture.supplyAsync(() -> { + return CompletableFuture.supplyAsync(() -> { + return processInput(input); + }); + }); + + // Now you have a nested CompletableFuture - hard to work with + } + + // BAD: Resource leak - no executor shutdown + private final ExecutorService executor = Executors.newFixedThreadPool(10); + + public CompletableFuture processWithLeakedExecutor(String input) { + return CompletableFuture.supplyAsync(() -> processInput(input), executor); + // BAD: Executor never gets shut down + } + + private String processInput(String input) { + try { + Thread.sleep(1000); // Simulate work + } catch (InterruptedException e) { + // BAD: Not handling interruption properly + } + return "processed_" + input; + } +}]]> + + + + + + + Embrace Virtual Threads for Enhanced Scalability + Use Virtual Threads for I/O-bound Tasks + + + Leverage virtual threads (Project Loom) for I/O-bound tasks to dramatically increase scalability with minimal resource overhead. Avoid pooling virtual threads and use structured concurrency where appropriate. + + + + > futures = IntStream.range(0, 10000) + .mapToObj(i -> virtualExecutor.submit(() -> performIOOperation("task-" + i))) + .toList(); + + // Collect results + futures.forEach(future -> { + try { + String result = future.get(); + System.out.println("Completed: " + result); + } catch (Exception e) { + System.err.println("Task failed: " + e.getMessage()); + } + }); + } + + // Virtual threads are perfect for blocking I/O operations + private String performIOOperation(String taskId) { + try { + // Simulate I/O operation (database call, web request, etc.) + Thread.sleep(1000); // This would block a platform thread + return "Result for " + taskId; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return "Interrupted: " + taskId; + } + } + + // Using scoped values with virtual threads (Java 20+) + private static final ScopedValue USER_ID = ScopedValue.newInstance(); + + public void processWithScopedValue(String userId, List tasks) { + // Run with scoped value + ScopedValue.where(USER_ID, userId) + .run(() -> { + tasks.parallelStream().forEach(task -> { + virtualExecutor.submit(() -> { + // Access scoped value safely + String currentUserId = USER_ID.get(); + performTaskForUser(currentUserId, task); + }); + }); + }); + } + + private void performTaskForUser(String userId, String task) { + System.out.println("Processing task " + task + " for user " + userId + + " on thread " + Thread.currentThread()); + } + + // Structured concurrency for managing related tasks + public String fetchUserDataWithStructuredConcurrency(String userId) throws Exception { + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + + Future profile = scope.fork(() -> fetchUserProfile(userId)); + Future preferences = scope.fork(() -> fetchUserPreferences(userId)); + Future history = scope.fork(() -> fetchUserHistory(userId)); + + scope.join(); // Wait for all tasks + scope.throwIfFailed(); // Propagate any failures + + // All tasks completed successfully + return combineUserData(profile.resultNow(), + preferences.resultNow(), + history.resultNow()); + } + } + + private String fetchUserProfile(String userId) { + // Simulate I/O operation + try { Thread.sleep(100); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return "Profile for " + userId; + } + + private String fetchUserPreferences(String userId) { + try { Thread.sleep(150); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return "Preferences for " + userId; + } + + private String fetchUserHistory(String userId) { + try { Thread.sleep(200); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return "History for " + userId; + } + + private String combineUserData(String profile, String preferences, String history) { + return String.format("User data: %s, %s, %s", profile, preferences, history); + } + + public void shutdown() { + virtualExecutor.shutdown(); + } +}]]> + + + { + // BAD: Virtual threads are not suitable for CPU-bound work + double result = 0; + for (int j = 0; j < 1_000_000; j++) { + result += Math.sqrt(j) * Math.sin(j); + } + return result; + }); + } + } + + // BAD: Using platform thread patterns with virtual threads + public void badResourceManagement() { + // Creating virtual threads manually instead of using executor + for (int i = 0; i < 10000; i++) { + Thread.ofVirtual().start(() -> { + performIOOperation(); + // BAD: No proper cleanup or error handling + }); + } + } + + // BAD: Blocking operations that shouldn't be used with virtual threads + public void problematicBlocking() { + ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor(); + + virtualExecutor.submit(() -> { + try { + synchronized (this) { // BAD: Synchronized blocks can pin virtual threads + Thread.sleep(1000); // This pins the virtual thread to platform thread + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + } + + // BAD: Using ThreadLocal instead of ScopedValue + private static final ThreadLocal USER_CONTEXT = new ThreadLocal<>(); + + public void badContextPropagation() { + USER_CONTEXT.set("user123"); + + ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor(); + virtualExecutor.submit(() -> { + // BAD: ThreadLocal values don't propagate to virtual threads properly + String userId = USER_CONTEXT.get(); // Likely null + performTaskForUser(userId); + }); + } + + private void performIOOperation() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void performTaskForUser(String userId) { + System.out.println("Processing for user: " + userId); + } +}]]> + + + + +
diff --git a/spml/src/main/resources/126-java-logging.xml b/spml/src/main/resources/126-java-logging.xml new file mode 100644 index 00000000..e3737004 --- /dev/null +++ b/spml/src/main/resources/126-java-logging.xml @@ -0,0 +1,1009 @@ + + + + Java Logging Best Practices + *.java + false + + java + logging + slf4j + logback + monitoring + security + performance + + 1.0.0 + + +
+ Java Logging Best Practices +
+ + + You are a Senior software engineer with extensive experience in Java software development + + + + Effective Java logging involves selecting a standard framework (SLF4J with Logback/Log4j2), using appropriate log levels (ERROR, WARN, INFO, DEBUG, TRACE), and adhering to core practices like parameterized logging, proper exception handling, and avoiding sensitive data exposure. Configuration should be environment-specific with clear output formats. Security is paramount: mask sensitive data, control log access, and ensure secure transmission. Implement centralized log aggregation, monitoring, and alerting for proactive issue detection. Finally, logging behavior and its impact should be validated through comprehensive testing. + + + + + + + + Choose an Appropriate Logging Framework + Select a Standard Logging Facade and Implementation + + + Using a standard logging facade like SLF4J allows for flexibility in choosing and switching an underlying logging implementation. The primary recommendation is SLF4J with Logback for its robustness and feature-richness. + + + + + + + + + + + + + + Understand and Use Logging Levels Correctly + Apply Appropriate Logging Levels for Messages + + + Use logging levels consistently to categorize the severity and importance of log messages. ERROR for critical issues, WARN for potentially harmful situations, INFO for important business events, DEBUG for detailed information, and TRACE for fine-grained debugging. + + + + + + + + + + + + + + Adhere to Core Logging Practices + Implement Fundamental Best Practices + + + Follow core practices including using parameterized logging, proper exception handling, avoiding sensitive data exposure, and implementing performance considerations for logging operations. + + + + dataItems) { + logger.info("Processing {} data items", dataItems.size()); + + for (int i = 0; i < dataItems.size(); i++) { + String item = dataItems.get(i); + + try { + processDataItem(item); + + // GOOD: Log progress periodically, not for every item + if (i % 1000 == 0) { + logger.debug("Processed {} of {} items", i, dataItems.size()); + } + + } catch (Exception e) { + // GOOD: Log error but continue processing + logger.warn("Failed to process item at index {}: {}", i, item, e); + } + } + + logger.info("Completed processing {} data items", dataItems.size()); + } + + // Utility methods + private String maskCreditCard(String cardNumber) { + if (cardNumber == null || cardNumber.length() < 8) { + return "****"; + } + return cardNumber.substring(0, 4) + "****" + cardNumber.substring(cardNumber.length() - 4); + } + + private String generateCorrelationId() { + return "TXN-" + System.currentTimeMillis() + "-" + Thread.currentThread().getId(); + } + + private String buildComplexDebugInfo(String userId, String amount) { + // Simulate expensive debug information building + return String.format("User: %s, Amount: %s, Timestamp: %d", + userId, amount, System.currentTimeMillis()); + } + + private void validatePaymentRequest(String userId, String amount, String cardNumber) + throws ValidationException { + if (Objects.isNull(userId) || userId.trim().isEmpty()) { + throw new ValidationException("INVALID_USER_ID"); + } + // More validation... + } + + private void processPaymentInternal(String userId, String amount, String cardNumber) + throws PaymentProcessingException { + // Simulate payment processing + if ("fail".equals(userId)) { + throw new PaymentProcessingException("Payment gateway error"); + } + } + + private void processDataItem(String item) { + // Simulate data processing + if ("error".equals(item)) { + throw new RuntimeException("Processing failed for item: " + item); + } + } + + // Exception classes + private static class ValidationException extends Exception { + private final String validationError; + + public ValidationException(String validationError) { + super("Validation failed: " + validationError); + this.validationError = validationError; + } + + public String getValidationError() { + return validationError; + } + } + + private static class PaymentProcessingException extends RuntimeException { + public PaymentProcessingException(String message) { + super(message); + } + + public PaymentProcessingException(String message, Throwable cause) { + super(message, cause); + } + } +}]]> + + + items) { + // BAD: Logging every single item in large dataset + for (String item : items) { + logger.debug("Processing item: " + item); // Will flood logs + processItem(item); + logger.debug("Completed item: " + item); // Even more noise + } + } + + public void handleUserLogin(String username, String password) { + // BAD: Logging passwords - NEVER do this! + logger.debug("User login attempt: username=" + username + ", password=" + password); + + try { + authenticateUser(username, password); + // BAD: Inconsistent logging format + logger.info("User " + username + " logged in successfully"); + } catch (AuthenticationException e) { + // BAD: Using wrong log level and exposing sensitive info + logger.error("Login failed for " + username + " with password " + password); + } + } + + public void performDatabaseOperation(String query, String connectionString) { + // BAD: Logging database connection strings (may contain credentials) + logger.debug("Executing query: " + query + " on connection: " + connectionString); + + try { + executeQuery(query); + } catch (SQLException e) { + // BAD: Not using parameterized logging with exception + logger.error("SQL error: " + e.getMessage() + " for query: " + query); + // Stack trace is lost! + } + } + + // BAD: No try-with-resources or proper cleanup for MDC + public void badMDCUsage(String userId) { + org.slf4j.MDC.put("userId", userId); + logger.info("Processing for user"); + + // ... some processing ... + + // BAD: Forgot to clear MDC - memory leak! + // MDC.clear() or MDC.remove("userId") is missing + } + + // Helper methods with poor exception handling + private String buildExpensiveDebugString(String userId, String amount, String cardNumber) { + // Simulate expensive operation + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10000; i++) { + sb.append("Debug info for ").append(userId).append(" "); + } + return sb.toString(); + } + + private void validatePayment(String userId, String amount) throws ValidationException { + if (userId == null) throw new ValidationException("Invalid user"); + } + + private void processPaymentTransaction(String userId, String amount, String cardNumber) { + // Simulate processing + } + + private void processItem(String item) { + // Simulate processing + } + + private void authenticateUser(String username, String password) throws AuthenticationException { + if ("baduser".equals(username)) { + throw new AuthenticationException("Invalid credentials"); + } + } + + private void executeQuery(String query) throws SQLException { + if (query.contains("DROP")) { + throw new SQLException("Invalid query"); + } + } + + // Exception classes + private static class ValidationException extends Exception { + public ValidationException(String message) { super(message); } + } + private static class AuthenticationException extends Exception { + public AuthenticationException(String message) { super(message); } + } + private static class SQLException extends Exception { + public SQLException(String message) { super(message); } + } +}]]> + + + + + + + Implement Secure Logging Practices + Ensure Logs Do Not Compromise Security + + + Actively mask or filter sensitive data, control access to log files, use secure transmission protocols, and comply with data protection regulations when logging information. + + + + \"'&]", "*"); + + // Limit length to prevent log flooding + if (sanitized.length() > 100) { + sanitized = sanitized.substring(0, 97) + "..."; + } + + return sanitized; + } + + private String maskEmail(String email) { + if (email == null || !EMAIL_PATTERN.matcher(email).matches()) { + return "[INVALID_EMAIL]"; + } + + int atIndex = email.indexOf('@'); + if (atIndex < 2) { + return "**@" + email.substring(atIndex + 1); + } + + return email.substring(0, 2) + "***@" + email.substring(atIndex + 1); + } + + private String maskCreditCard(String cardNumber) { + if (cardNumber == null || cardNumber.length() < 8) { + return "[INVALID_CARD]"; + } + + String digitsOnly = cardNumber.replaceAll("[^\\d]", ""); + if (digitsOnly.length() < 8) { + return "[INVALID_CARD]"; + } + + return digitsOnly.substring(0, 4) + "****" + + digitsOnly.substring(digitsOnly.length() - 4); + } + + private String hashSensitiveData(String data) { + if (data == null) return "[null]"; + + // Use a secure hash for audit purposes (not for security) + return "HASH_" + Math.abs(data.hashCode()); + } + + private String generateSecureCorrelationId() { + return "CORR_" + System.currentTimeMillis() + "_" + + Thread.currentThread().getId(); + } + + // Mock classes and methods + private void validateUserData(String username, String email, String ssn, String creditCard) + throws ValidationException { + if (username == null || username.trim().isEmpty()) { + throw new ValidationException("INVALID_USERNAME"); + } + } + + private void createUserAccount(String username, String email) { + // Account creation logic + } + + private void validatePaymentMethod(String paymentMethod) { + // Payment validation logic + } + + private ChargeResult processCharge(PaymentRequest request) throws PaymentException { + // Payment processing logic + return new ChargeResult("TXN_" + System.currentTimeMillis()); + } + + // Mock classes + private static class PaymentRequest { + private String amount = "100.00"; + private String paymentMethod = "card"; + private String merchantId = "MERCHANT_123"; + + public String getAmount() { return amount; } + public String getPaymentMethod() { return paymentMethod; } + public String getMerchantId() { return merchantId; } + } + + private static class ChargeResult { + private final String transactionId; + public ChargeResult(String transactionId) { this.transactionId = transactionId; } + public String getTransactionId() { return transactionId; } + } + + private static class ValidationException extends Exception { + private final String errorCode; + public ValidationException(String errorCode) { + super("Validation failed: " + errorCode); + this.errorCode = errorCode; + } + public String getErrorCode() { return errorCode; } + } + + private static class SecurityException extends Exception { + private final String violationType; + public SecurityException(String violationType) { + super("Security violation: " + violationType); + this.violationType = violationType; + } + public String getViolationType() { return violationType; } + } + + private static class FraudDetectedException extends Exception { + private final int riskScore; + public FraudDetectedException(int riskScore) { + super("Fraud detected with risk score: " + riskScore); + this.riskScore = riskScore; + } + public int getRiskScore() { return riskScore; } + } + + private static class PaymentException extends Exception { + private final String errorType; + public PaymentException(String errorType) { + super("Payment error: " + errorType); + this.errorType = errorType; + } + public String getErrorType() { return errorType; } + } +}]]> + + + + + + + +
diff --git a/spml/src/main/resources/131-java-unit-testing.xml b/spml/src/main/resources/131-java-unit-testing.xml new file mode 100644 index 00000000..febc6713 --- /dev/null +++ b/spml/src/main/resources/131-java-unit-testing.xml @@ -0,0 +1,1177 @@ + + + + + + false + + java + unit-testing + junit5 + assertj + mockito + testing + best-practices + + 1.0.0 + + +
+ Java Unit testing guidelines +
+ + + You are a Senior software engineer with extensive experience in Java software development + + + + Effective Java unit testing involves using JUnit 5 annotations and AssertJ for fluent assertions. Tests should follow the Given-When-Then structure with descriptive names for clarity. Each test must have a single responsibility, be independent, and leverage parameterized tests for data variations. Mocking dependencies with frameworks like Mockito is crucial for isolating the unit under test. While code coverage is a useful guide, the focus should be on meaningful tests for critical logic and edge cases. Test classes and methods should typically be package-private. Strategies for code splitting include small test methods and helper functions. Anti-patterns like testing implementation details, hard-coded values, and ignoring failures should be avoided. Proper state management involves isolated state and immutable objects, and error handling should include testing for expected exceptions and their messages. + + + + + + + + Use JUnit 5 Annotations + Prefer JUnit 5 annotations over JUnit 4 + + + Utilize annotations from the `org.junit.jupiter.api` package (e.g., `@Test`, `@BeforeEach`, `@AfterEach`, `@DisplayName`, `@Nested`, `@Disabled`) instead of their JUnit 4 counterparts (`@org.junit.Test`, `@Before`, `@After`, `@Ignore`). This ensures consistency and allows leveraging the full capabilities of JUnit 5. + + + + + + + + + + + + + + Use AssertJ for Assertions + Prefer AssertJ for assertions + + + Employ AssertJ's fluent API (`org.assertj.core.api.Assertions.assertThat`) for more readable, expressive, and maintainable assertions compared to JUnit Jupiter's `Assertions` class or Hamcrest matchers. + + + + service.divide(1, 0)) // Preferred way to test exceptions + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("zero"); + } +}]]> + + + service.divide(1, 0) + ); + assertTrue(exception.getMessage().contains("zero")); // Separate assertion for message + } +}]]> + + + + + + + Structure Tests with Given-When-Then + Structure test methods using the Given-When-Then pattern + + + Organize the logic within test methods into three distinct, clearly separated phases: **Given** (setup preconditions), **When** (execute the code under test), and **Then** (verify the outcome). Use comments or empty lines to visually separate these phases, enhancing readability and understanding of the test's purpose. + + + + + + + + + + + + + + Use Descriptive Test Names + Write descriptive test method names or use `@DisplayName` + + + Test names should clearly communicate the scenario being tested and the expected outcome. Use either descriptive method names (e.g., following the `should_ExpectedBehavior_when_StateUnderTest` pattern) or JUnit 5's `@DisplayName` annotation for more natural language descriptions. This makes test reports easier to understand. + + + + calculator.divide(1, 0)) + .isInstanceOf(ArithmeticException.class); +} + +@Test +@DisplayName("Should return the correct sum for positive numbers") +void additionWithPositives() { + // Given + Calculator calculator = new Calculator(); + int num1 = 5; + int num2 = 10; + + // When + int actualSum = calculator.add(num1, num2); + + // Then + assertThat(actualSum).isEqualTo(15); +}]]> + + + + + + + + + + Aim for Single Responsibility in Tests + Each test method should verify a single logical concept + + + Avoid testing multiple unrelated things within a single test method. Each test should focus on one specific aspect of the unit's behavior or one particular scenario. This makes tests easier to understand, debug, and maintain. If a test fails, its specific focus makes pinpointing the cause simpler. + + + + + + + + + + + + + + Ensure Tests are Independent + Tests must be independent and runnable in any order + + + Avoid creating tests that depend on the state left behind by previously executed tests. Each test should set up its own required preconditions (using `@BeforeEach` or within the test method itself) and should not rely on the execution order. This ensures test suite stability and reliability, preventing flickering tests. + + + + found = repository.findById("testId"); + + // Then + assertThat(found).isPresent(); + } + + @Test + void should_returnEmpty_when_itemDoesNotExist() { + // Given - Repository is clean (or re-initialized via @BeforeEach) + + // When + Optional found = repository.findById("nonExistentId"); + + // Then + assertThat(found).isNotPresent(); + } +}]]> + + + found = repository.findById("testId"); + assertThat(found).isPresent(); + } +}]]> + + + + + + + Use Parameterized Tests for Data Variations + Use `@ParameterizedTest` for testing the same logic with different inputs + + + When testing a method's behavior across various input values or boundary conditions, leverage JUnit 5's parameterized tests (`@ParameterizedTest` with sources like `@ValueSource`, `@CsvSource`, `@MethodSource`). This avoids code duplication and clearly separates the test logic from the test data. + + + + + + + + + + + + + + Utilize Mocking for Dependencies (Mockito) + Isolate the unit under test using mocking frameworks like Mockito + + + Unit tests should focus solely on the logic of the class being tested (System Under Test - SUT), not its dependencies (database, network services, other classes). Use mocking frameworks like Mockito to create mock objects that simulate the behavior of these dependencies. This ensures tests are fast, reliable, and truly test the unit in isolation. + + + Key Mockito Concepts: + + `mock(Class<T> classToMock)` + Creates a mock object of a given class or interface. + + + `when(mock.methodCall()).thenReturn(value)` + Defines the behavior of a mock object's method. When the specified method is called on the mock, it will return the defined `value`. + + + `verify(mock).methodCall()` + Verifies that a specific method was called on the mock object. You can also specify the number of times (`times(n)`), at least once (`atLeastOnce()`), etc. + + + `@Mock` Annotation + Used with `@ExtendWith(MockitoExtension.class)` (JUnit 5) to automatically create mocks for fields. + + + `@InjectMocks` Annotation + Creates an instance of the class under test and automatically injects fields annotated with `@Mock` into it. + + + + + actualUser = userService.findUserById("123"); + + // Then + assertThat(actualUser).isPresent().contains(expectedUser); + // Verify that findById("123") was called exactly once on the mock repository + verify(userRepository, times(1)).findById("123"); + verifyNoMoreInteractions(userRepository); // Optional: ensure no other methods were called + } + + @Test + @DisplayName("Should return empty optional when user not found") + void findUserById_NotFound() { + // Given + // Define mock behavior: when findById is called with any string, return empty + when(userRepository.findById(anyString())).thenReturn(Optional.empty()); + + // When + Optional actualUser = userService.findUserById("unknownId"); + + // Then + assertThat(actualUser).isNotPresent(); + verify(userRepository).findById("unknownId"); // Verify the specific call + } + + @Test + @DisplayName("Should save user successfully") + void saveUser() { + // Given + User userToSave = new User(null, "Jane Doe"); // ID might be generated on save + User savedUser = new User("genId", "Jane Doe"); + // Define behavior for save: return the user with an ID + when(userRepository.save(any(User.class))).thenReturn(savedUser); + + // When + User result = userService.saveUser(userToSave); + + // Then + assertThat(result).isEqualTo(savedUser); + // Verify that save was called with the correct user object (or use ArgumentCaptor for complex cases) + verify(userRepository).save(userToSave); + } +}]]> + + + user = userService.findUserById("123"); + + // Test is slow, brittle, and doesn't isolate the unit under test + assertThat(user).isPresent(); + } +}]]> + + + + + + + Consider Test Coverage, But Don't Obsess + Use code coverage as a guide, not a definitive quality metric + + + Tools like JaCoCo can measure which lines of code are executed by your tests (code coverage). Aiming for high coverage (e.g., >80% line/branch coverage) is generally good practice, as it indicates most code paths are tested. However, 100% coverage doesn't guarantee bug-free code or high-quality tests. Focus on writing meaningful tests for critical logic and edge cases rather than solely chasing coverage numbers. A test might cover a line but not actually verify its correctness effectively. + + + + + + Test Scopes + Test classes and methods should be package-private + + + Test classes should have package-private visibility. There is no need for them to be public. Test methods should have package-private visibility. There is no need for them to be public. + + + + + + Code Splitting Strategies + Keep test methods focused and use proper organization + + + Small Test Methods: Keep test methods small and focused on testing a single behavior. Helper Methods: Use helper methods to avoid code duplication in test setup and assertions. Parameterized Tests: Utilize JUnit's parameterized tests to test the same logic with different input values. + + + + + + Anti-patterns and Code Smells + Avoid common testing anti-patterns + + + Testing Implementation Details: Avoid testing implementation details that might change, leading to brittle tests. Focus on testing behavior and outcomes. Hard-coded Values: Avoid hard-coding values in tests. Use constants or test data to make tests more maintainable. Complex Test Logic: Keep test logic simple and avoid complex calculations or conditional statements within tests. Ignoring Edge Cases: Don't ignore edge cases or boundary conditions. Ensure tests cover a wide range of inputs, including invalid or unexpected values. Slow Tests: Avoid slow tests that discourage developers from running them frequently. Over-reliance on Mocks: Mock judiciously; too many mocks can obscure the actual behavior and make tests less reliable. Ignoring Test Failures: Never ignore failing tests. Investigate and fix them promptly. + + + + + + State Management + Ensure proper state isolation between tests + + + - **Isolated State:** Ensure each test has its own isolated state to avoid interference between tests. Use `@BeforeEach` to reset the state before each test. + - **Immutable Objects:** Prefer immutable objects to simplify state management and avoid unexpected side effects. + - **Stateless Components:** Design stateless components whenever possible to reduce the need for state management in tests. + + + + + + Error Handling + Test error conditions and exception handling + + + - **Expected Exceptions:** Use AssertJ's `assertThatThrownBy` to verify that a method throws the expected exception under specific conditions. + - **Exception Messages:** Assert the exception message to ensure the correct error is being thrown with helpful context. + - **Graceful Degradation:** Test how the application handles errors and gracefully degrades when dependencies are unavailable. + + + + + + Leverage JSpecify for Null Safety + Utilize JSpecify annotations for explicit nullness contracts + + + Employ JSpecify annotations (org.jspecify.annotations.*) such as @NullMarked, @Nullable, and @NonNull to clearly define the nullness expectations of method parameters, return types, and fields within your tests and the code under test. This practice enhances code clarity, enables static analysis tools to catch potential NullPointerExceptions early, and improves the overall robustness of your tests and application code. In test code, this is particularly important for defining the expected behavior of mocks and verifying interactions with potentially null values. + + + + + + + + + + + + + + Key Questions to Guide Test Creation (RIGHT-BICEP) + Use RIGHT-BICEP to guide comprehensive test creation + + + Key Questions to Guide Test Creation + + + Description: + + If the code ran correctly, how would I know? + + + + How am I going to test this? + + + + What else can go wrong? + + + + Could this same kind of problem happen anywhere else? + + + + What to Test: Use Your RIGHT-BICEP + + + + Are the results **R**ight? + + + + Are all the **B**oundary conditions CORRECT? + + + + Can you check **I**nverse relationships? + + + + Can you **C**ross-check results using other means? + + + + Can you force **E**rror conditions to happen? + + + + Are **P**erformance characteristics within bounds? + + + + + + Integer.MAX_VALUE || (long)a + b < Integer.MIN_VALUE) { + throw new ArithmeticException("Integer overflow"); + } + return a + b; + } +} + +// Test class +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class CalculatorTest { + + private final Calculator calculator = new Calculator(); + + // R - Right results + @Test + void add_simplePositiveNumbers_returnsCorrectSum() { + assertThat(calculator.add(2, 3)).isEqualTo(5); + } + + // B - Boundary conditions (e.g., with zero, negative numbers, max/min values) + @Test + void add_numberAndZero_returnsNumber() { + assertThat(calculator.add(5, 0)).isEqualTo(5); + } + + @Test + void add_positiveAndNegative_returnsCorrectSum() { + assertThat(calculator.add(5, -4)).isEqualTo(1); + } + + @Test + void add_nearMaxInteger_returnsCorrectSum() { + assertThat(calculator.add(Integer.MAX_VALUE - 1, 1)).isEqualTo(Integer.MAX_VALUE); + } + + // I - Inverse relationships (e.g., subtraction) + // (Not directly applicable for a simple add, but if we had subtract: result - b == a) + + // C - Cross-check (e.g., add(a,b) == add(b,a)) + @Test + void add_commutativeProperty_holdsTrue() { + assertThat(calculator.add(2, 3)).isEqualTo(calculator.add(3, 2)); + } + + // E - Error conditions (e.g., overflow) + @Test + void add_integerOverflow_throwsArithmeticException() { + assertThatThrownBy(() -> calculator.add(Integer.MAX_VALUE, 1)) + .isInstanceOf(ArithmeticException.class) + .hasMessageContaining("overflow"); + } + + @Test + void add_integerUnderflow_throwsArithmeticException() { + assertThatThrownBy(() -> calculator.add(Integer.MIN_VALUE, -1)) + .isInstanceOf(ArithmeticException.class) + .hasMessageContaining("overflow"); + } + + // P - Performance (Not typically unit tested unless specific requirements) + // @Test + // void add_performance_isWithinAcceptableLimits() { + // // Potentially a more complex performance test scenario + // } +}]]> + + + + + + + + + + Characteristics of Good Tests (A-TRIP) + Good tests are A-TRIP + + +Good tests are A-TRIP: +- **A**utomatic: Tests should run without human intervention. +- **T**horough: Test everything that could break; cover edge cases. +- **R**epeatable: Tests should produce the same results every time, in any environment. +- **I**ndependent: Tests should not rely on each other or on the order of execution. +- **P**rofessional: Test code is real code; keep it clean, maintainable, and well-documented. + + + + items; + + public OrderProcessor() { + this.items = new ArrayList<>(); + } + + public void addItem(String item) { + if (Objects.isNull(item) || item.trim().isEmpty()) { + throw new IllegalArgumentException("Item cannot be null or empty"); + } + this.items.add(item); + } + + public int getItemCount() { + return this.items.size(); + } + + public void clearItems() { + this.items.clear(); + } +} + +// Test class demonstrating A-TRIP +public class OrderProcessorTest { + + private OrderProcessor processor; + + // Automatic: Part of JUnit test suite, runs with build tools. + // Independent: Each test sets up its own state. + @BeforeEach + void setUp() { + processor = new OrderProcessor(); // Fresh instance for each test + } + + // Thorough: Testing adding valid items. + @Test + void addItem_validItem_increasesCount() { + processor.addItem("Laptop"); + assertThat(processor.getItemCount()).isEqualTo(1); + processor.addItem("Mouse"); + assertThat(processor.getItemCount()).isEqualTo(2); + } + + // Thorough: Testing an edge case (adding null). + @Test + void addItem_nullItem_throwsIllegalArgumentException() { + assertThatThrownBy(() -> processor.addItem(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + // Thorough: Testing another edge case (adding empty string). + @Test + void addItem_emptyItem_throwsIllegalArgumentException() { + assertThatThrownBy(() -> processor.addItem(" ")) + .isInstanceOf(IllegalArgumentException.class); + } + + // Repeatable: No external dependencies that change (e.g., time, random numbers without seed). + // This test will always pass or always fail consistently. + @Test + void getItemCount_afterClearing_isZero() { + processor.addItem("Keyboard"); + processor.clearItems(); + assertThat(processor.getItemCount()).isEqualTo(0); + } + + // Professional: Code is clean, uses meaningful names, follows conventions. +}]]> + + + + + + + + + + Verifying CORRECT Boundary Conditions + Ensure your tests check CORRECT boundary conditions + + +Ensure your tests check the following boundary conditions (CORRECT): +- **C**onformance: Does the value conform to an expected format? (e.g., email format, date format) +- **O**rdering: Is the set of values ordered or unordered as appropriate? (e.g., sorted list, items in a queue) +- **R**ange: Is the value within reasonable minimum and maximum values? (e.g., age > 0, percentage 0-100) +- **R**eference: Does the code reference anything external that isn't under direct control of the code itself? (e.g., file existence, network connection - often for integration tests, but can apply to unit tests with mocks) +- **E**xistence: Does the value exist? (e.g., is non-null, non-zero, present in a set) +- **C**ardinality: Are there exactly enough values? (e.g., expecting 1 result, 0 results, N results) +- **T**ime (absolute and relative): Is everything happening in order? At the right time? In time? (e.g., timeouts, sequence of events) + + + + = MIN_AGE && age <= MAX_AGE; + } + + public boolean isValidEmailFormat(String email) { + if (Objects.isNull(email)) return false; + // C - Conformance (simplified regex for demonstration) + return email.matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"); + } + + // E - Existence + public boolean processUsername(String username) { + if (Objects.isNull(username) || username.trim().isEmpty()) { + // Checks for existence (non-null, non-empty) + return false; + } + // process username + return true; + } +} + +// Test class +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import static org.assertj.core.api.Assertions.assertThat; + +public class UserValidationTest { + private final UserValidation validator = new UserValidation(); + + // Testing Range for age + @Test + void isAgeValid_ageAtLowerBound_returnsTrue() { + assertThat(validator.isAgeValid(18)).isTrue(); + } + + @Test + void isAgeValid_ageAtUpperBound_returnsTrue() { + assertThat(validator.isAgeValid(120)).isTrue(); + } + + @Test + void isAgeValid_ageWithinBounds_returnsTrue() { + assertThat(validator.isAgeValid(35)).isTrue(); + } + + @Test + void isAgeValid_ageBelowLowerBound_returnsFalse() { + assertThat(validator.isAgeValid(17)).isFalse(); + } + + @Test + void isAgeValid_ageAboveUpperBound_returnsFalse() { + assertThat(validator.isAgeValid(121)).isFalse(); + } + + // Testing Conformance for email + @ParameterizedTest + @ValueSource(strings = {"user@example.com", "user.name@sub.example.co.uk"}) + void isValidEmailFormat_validEmails_returnsTrue(String email) { + assertThat(validator.isValidEmailFormat(email)).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = {"userexample.com", "user@", "@example.com", "user @example.com", ""}) + void isValidEmailFormat_invalidEmails_returnsFalse(String email) { + assertThat(validator.isValidEmailFormat(email)).isFalse(); + } + + @Test + void isValidEmailFormat_nullEmail_returnsFalse() { + assertThat(validator.isValidEmailFormat(null)).isFalse(); + } + + // Testing Existence for username + @Test + void processUsername_validUsername_returnsTrue() { + assertThat(validator.processUsername("john_doe")).isTrue(); + } + + @Test + void processUsername_nullUsername_returnsFalse() { + assertThat(validator.processUsername(null)).isFalse(); + } + + @Test + void processUsername_emptyUsername_returnsFalse() { + assertThat(validator.processUsername("")).isFalse(); + } + + @Test + void processUsername_blankUsername_returnsFalse() { + assertThat(validator.processUsername(" ")).isFalse(); + } +}]]> + + + + + + + +
diff --git a/spml/src/main/resources/141-java-refactoring-with-modern-features.xml b/spml/src/main/resources/141-java-refactoring-with-modern-features.xml new file mode 100644 index 00000000..ddeec9d2 --- /dev/null +++ b/spml/src/main/resources/141-java-refactoring-with-modern-features.xml @@ -0,0 +1,1138 @@ + + + + + + false + + java + refactoring + modern-features + java8+ + lambda + streams + optional + best-practices + + 1.0.0 + + +
+ Modern Java Development Guidelines (Java 8+) +
+ + + You are a Senior software engineer with extensive experience in Java software development + + + + Modern Java development (Java 8+) emphasizes leveraging lambda expressions and functional interfaces over anonymous classes, and using the Stream API for declarative collection processing. The `Optional` API should be used for handling potentially absent values gracefully, and the `java.time` API for all date/time operations. Default methods allow non-breaking interface evolution. Local Variable Type Inference (`var`) can improve readability when used judiciously. Unmodifiable collection factory methods (`List.of()`, etc.) provide concise immutable collections. `CompletableFuture` facilitates composable asynchronous programming. The Java Platform Module System (JPMS, Java 9+) enables strong encapsulation. Performance implications of new features should be considered and profiled. Testing strategies need to adapt to these modern features, and text blocks (Java 15+) offer improved readability for multi-line strings. + + + + + + + + Lambda Expressions and Functional Interfaces + Effectively Use Lambda Expressions and Functional Interfaces + + + - Prefer lambda expressions over anonymous inner classes for concise implementation of functional interfaces. + - Keep lambda expressions short, readable, and focused on a single piece of logic. + - Use method references (e.g., `System.out::println`, `String::isEmpty`) when they are clearer and more direct than an equivalent lambda. + - Leverage the rich set of built-in functional interfaces from the `java.util.function` package (e.g., `Predicate`, `Function`, `Consumer`, `Supplier`). + - Create custom functional interfaces only when a specific signature is needed that isn't covered by built-in ones. + - Always annotate custom functional interfaces with `@FunctionalInterface` to ensure they meet the criteria (a single abstract method) and to clearly communicate their purpose. + + + + { + R process(T data); +} + +public class LambdaExample { + public static void main(String[] args) { + List names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve"); + + // Good: Using method reference + System.out.println("Printing names using method reference:"); + names.forEach(System.out::println); + + // Good: Simple lambda for filtering + List longNames = names.stream() + .filter(str -> str.length() > 4) // Lambda expression + .collect(Collectors.toList()); + System.out.println("\nLong names (length > 4): " + longNames); + + // Good: Using a built-in functional interface (Predicate) + Predicate startsWithA = s -> s.startsWith("A"); + List namesStartingWithA = names.stream() + .filter(startsWithA) + .collect(Collectors.toList()); + System.out.println("Names starting with 'A': " + namesStartingWithA); + + // Good: Using a custom functional interface + DataProcessor nameLengthProcessor = name -> name.length(); + int lengthOfAlice = nameLengthProcessor.process("Alice"); + System.out.println("Length of 'Alice' using custom processor: " + lengthOfAlice); + } +}]]> + + + names = Arrays.asList("Alice", "Bob"); + + // Bad: Using an anonymous inner class where a lambda would be more concise + names.forEach(new Consumer() { + @Override + public void accept(String s) { + System.out.println("Processing: " + s); + } + }); + + // Bad: Overly complex lambda that should be a separate method + names.stream().filter(s -> { + System.out.println("Checking name: " + s); // Side effect in filter + boolean isLong = s.length() > 3; + boolean startsWithVowel = "AEIOUaeiou".indexOf(s.charAt(0)) != -1; + return isLong && startsWithVowel; // Multiple conditions + }).forEach(System.out::println); + } +}]]> + + + + + + + Stream API + Leverage the Stream API for Collection Processing + + + Use streams for processing sequences of elements from collections or other sources in a functional style. Prefer a declarative approach (what to do) over an imperative one (how to do it) for stream operations. Chain stream operations effectively to create a readable processing pipeline. Use appropriate terminal operations to produce a result or side-effect. Be mindful of performance implications, especially with large datasets or complex operations. + + + + data = Arrays.asList(" apple ", null, " BANANA ", " cherry ", "apple", " "); + + // Good: Effective stream usage for cleaning and processing data + List processedData = data.stream() + .filter(Objects::nonNull) // Remove nulls + .map(String::trim) // Trim whitespace + .filter(s -> !s.isEmpty()) // Remove empty strings + .map(String::toLowerCase) // Convert to lower case + .distinct() // Keep unique elements + .sorted() // Sort alphabetically + .collect(Collectors.toList()); + System.out.println("Processed and sorted data: " + processedData); + + // Good: Complex data transformation pipeline + List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + int sumOfEvenSquares = numbers.stream() + .filter(n -> n % 2 == 0) + .mapToInt(n -> n * n) + .sum(); + System.out.println("Sum of squares of even numbers: " + sumOfEvenSquares); + } +}]]> + + + data = Arrays.asList("a", "b", "c"); + List result = new ArrayList<>(); + + // Bad: Using stream for a simple loop where a foreach loop is clearer + // and potentially more performant for simple side effects on small lists. + data.stream().forEach(s -> result.add(s.toUpperCase())); // Modifying external list - side effect! + + // Better (imperative but clear for this simple case, or use map().collect()): + for (String s : data) { + result.add(s.toUpperCase()); + } + System.out.println("Uppercase (imperative): " + result); + + // Bad: Overusing parallel streams for simple operations on small collections + // The overhead of parallelization can outweigh benefits. + List numbers = Arrays.asList(1, 2, 3, 4, 5); + int sum = numbers.parallelStream() // Unnecessary parallelization for small sum + .reduce(0, Integer::sum); + System.out.println("Sum (parallel, overkill): " + sum); + } +}]]> + + + + + + + Optional API + Handle Potentially Absent Values Gracefully with Optional + + + Use `Optional<T>` to explicitly represent values that may be absent, making your API clearer about nullability. Avoid calling `Optional.get()` directly without first checking `isPresent()`. Prefer functional alternatives. Leverage `Optional`'s functional-style methods like `map()`, `flatMap()`, `filter()`, `orElse()`, `orElseGet()`, `orElseThrow()` to handle absent values in a fluent and safe manner. + + + + getAddress() { return Optional.ofNullable(address); } + public String getName() { return name; } +} +class Address { + Country country; + public Address(Country c) { this.country = c; } + public Optional getCountry() { return Optional.ofNullable(country); } +} +class Country { + String code; + public Country(String code) { this.code = code; } + public String getCode() { return code; } +} + +public class OptionalExample { + private static Map userRepository = Map.of( + "user1", new User("Alice", new Address(new Country("US"))), + "user2", new User("Bob", new Address(null)), // User with address, but no country + "user3", new User("Charlie", null) // User with no address + ); + + public static String findUserCountryCode(String userId) { + // Good: Complex Optional chain to safely navigate potentially null properties + return Optional.ofNullable(userRepository.get(userId)) // Optional + .flatMap(User::getAddress) // Optional
+ .flatMap(Address::getCountry) // Optional + .map(Country::getCode) // Optional (country code) + .orElse("UNKNOWN_COUNTRY"); // Default if any step results in empty + } + + public static String processValue(String value) { + // Good: Optional usage for simple processing + return Optional.ofNullable(value) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(String::toUpperCase) + .orElse("DEFAULT_VALUE"); + } +}]]> + + + optValue) { // Parameter is Optional - not ideal + // Bad: Calling .get() without isPresent() check - can throw NoSuchElementException + if (optValue.isPresent()) { // This check is good, but often people forget it + return optValue.get(); + } + return "default"; // Or one might just call optValue.get() directly + } + + public void process(String value) { + Optional optionalValue = Optional.ofNullable(value); + // Bad: Using isPresent() and get() where orElse/map could be used + if (optionalValue.isPresent()) { + String s = optionalValue.get(); + System.out.println("Value is: " + s.toUpperCase()); + } else { + System.out.println("Value is: DEFAULT"); + } + } + + // Optional field - generally not recommended + private Optional configuration = Optional.empty(); + public void setConfiguration(String config) { this.configuration = Optional.ofNullable(config); } +}]]> + + + + + + + Date/Time API (java.time) + Utilize the Modern java.time API for Dates and Times + + + Replace legacy `java.util.Date`, `java.util.Calendar`, and `java.text.SimpleDateFormat` with the classes from the `java.time` package (introduced in Java 8). Choose the appropriate `java.time` class for your specific needs: `Instant` for machine time, `LocalDate` for dates without time-zone, `LocalDateTime` for date-time without time-zone, `ZonedDateTime` for date-time with specific time-zone, `Duration` for time-based amounts, and `Period` for date-based amounts. + + + + + + + + + + + + + + Default Methods in Interfaces + Enhance Interfaces with Default Methods for Non-Breaking Evolution + + + Use default methods to add new functionality to existing interfaces without breaking implementing classes. Keep default method implementations simple and focused. Complex logic might be better suited for helper classes or abstract base classes. Clearly document the behavior of default methods, including any assumptions they make about the interface's contract. Avoid introducing state (fields) into interfaces, as default methods cannot access instance fields directly. + + + + + + + 0) { + result.append(part.trim().toUpperCase()); + if (!part.equals(parts[parts.length - 1])) { + result.append("|"); + } + } + } + // Complex validation + if (result.length() > 100) { + throw new IllegalArgumentException("Result too long"); + } + return result.toString(); + } +}]]> + + + + + + + Local Variable Type Inference (var) + Use var judiciously to improve readability + + + Use the `var` keyword (introduced in Java 10) for local variable declarations when the type is obvious from the context and it improves readability. Avoid using `var` when it makes the code less clear or when the inferred type might not be what you expect. Use `var` with care in complex expressions or when the type provides important documentation value. + + + + + + + or List + var data = getData(); + + // Bad: Return type is ambiguous + var result = processData(data); + + // Bad: Numeric literal type is not obvious (int? long? double?) + var number = 42; + var decimal = 3.14; + + // Bad: Method chain makes type unclear + var processed = data.stream() + .filter(item -> item != null) + .map(item -> item.toString()) + .findFirst(); + + // Bad: Using var with null (compilation error) + // var nullValue = null; // This won't compile + + // Bad: Diamond operator with var is redundant + // var list = new ArrayList<>(); // Type can't be inferred properly + } + + private List getData() { return List.of(); } + private Object processData(List data) { return new Object(); } +}]]> + + + + + + + Collection Factory Methods + Utilize Unmodifiable Collection Factory Methods + + + Use the static factory methods `List.of()`, `Set.of()`, and `Map.of()` (Java 9+) to create compact, unmodifiable (immutable) collections when the elements are known at compile time. For creating mutable collections, continue to use traditional constructors (e.g., `new ArrayList<>()`) or Stream API collectors. Be aware that these factory methods create unmodifiable collections, do not permit `null` elements, and `Map.of()` does not permit duplicate keys. + + + + unmodifiableList = List.of("alpha", "beta", "gamma"); + System.out.println("Unmodifiable List: " + unmodifiableList); + try { + unmodifiableList.add("delta"); // Throws UnsupportedOperationException + } catch (UnsupportedOperationException e) { + System.out.println("Error adding to unmodifiableList: " + e.getMessage()); + } + + Set unmodifiableSet = Set.of(10, 20, 30, 20); // Duplicate 20 is ignored for Set + System.out.println("Unmodifiable Set: " + unmodifiableSet); + + Map unmodifiableMap = Map.of( + "one", 1, + "two", 2, + "three", 3 + ); + System.out.println("Unmodifiable Map: " + unmodifiableMap); + + // Good: Creating mutable collections using traditional methods or collectors + List mutableListFromStream = Stream.of("x", "y", "z") + .filter(s -> s.length() == 1) + .collect(Collectors.toList()); // Creates a mutable ArrayList by default + mutableListFromStream.add("a"); + System.out.println("Mutable list from stream: " + mutableListFromStream); + + List anotherMutableList = new ArrayList<>(List.of("initial")); // Initialize mutable from unmodifiable + anotherMutableList.add("added"); + System.out.println("Mutable list initialized from List.of: " + anotherMutableList); + } +}]]> + + + listWithNull = List.of("apple", null, "banana"); + System.out.println(listWithNull); + } catch (NullPointerException e) { + System.err.println("Error: List.of() does not accept null elements. " + e.getMessage()); + } + + // Bad: Attempting to use Map.of() with duplicate keys (will throw IllegalArgumentException) + try { + Map mapWithDuplicateKeys = Map.of("a", 1, "b", 2, "a", 3); + System.out.println(mapWithDuplicateKeys); + } catch (IllegalArgumentException e) { + System.err.println("Error: Map.of() does not accept duplicate keys. " + e.getMessage()); + } + + // Misunderstanding: Thinking List.of() returns a general-purpose mutable list + List myList = List.of("one", "two"); + // myList.add("three"); // This would throw UnsupportedOperationException at runtime + // If mutability is needed, ArrayList should be used: + // List mutableMyList = new ArrayList<>(List.of("one", "two")); + // mutableMyList.add("three"); + } +}]]> + + + + + + + CompletableFuture for Asynchronous Programming + Employ CompletableFuture for Composable Asynchronous Operations + + + Use `CompletableFuture` (Java 8+) for managing sequences of asynchronous operations, avoiding callback hell and enabling a more functional, composable style. Chain asynchronous tasks using methods like `thenApplyAsync()`, `thenComposeAsync()`, `thenAcceptAsync()`, `thenRunAsync()`. Combine results from multiple `CompletableFuture` instances using `allOf()` or `anyOf()`. Handle exceptions gracefully using `exceptionally()` or `handle()`. Be mindful of the `Executor` used for each stage and consider timeout handling for asynchronous operations. + + + + futureSuccess = CompletableFuture + .supplyAsync(() -> fetchData("query1"), customExecutor) // Stage 1 on custom executor + .thenApplyAsync(data -> processData(data), customExecutor) // Stage 2 on custom executor + .thenAcceptAsync(result -> saveData(result), customExecutor) // Stage 3 on custom executor + .exceptionally(ex -> { // Handle exceptions from any preceding stage + System.err.println("" + Thread.currentThread().getName() + " Error in chain: " + ex.getMessage()); + return null; // Must return Void (or a compatible type for thenAccept) + }); + + CompletableFuture futureFetchFail = CompletableFuture + .supplyAsync(() -> fetchData("fail_fetch"), customExecutor) + .thenApplyAsync(data -> processData(data), customExecutor) + .thenAcceptAsync(result -> saveData(result), customExecutor) + .exceptionally(ex -> { + System.err.println("" + Thread.currentThread().getName() + " Error in fetch_fail chain: " + ex.getMessage()); + return null; + }); + + System.out.println("Futures submitted. Main thread continues..."); + + // Wait for futures to complete for demonstration purposes + futureSuccess.join(); + futureFetchFail.join(); + + customExecutor.shutdown(); + try { + if (!customExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + customExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + customExecutor.shutdownNow(); + } + System.out.println("All operations finished."); + } +}]]> + + + future = CompletableFuture.supplyAsync(() -> { + try { Thread.sleep(2000); } catch (InterruptedException e) {} + return "Slow result"; + }); + + String result = null; + try { + System.out.println("Blocking to get future result..."); + result = future.get(); // Blocking call - can make application unresponsive + System.out.println("Got result: " + result); + } catch (InterruptedException | ExecutionException e) { + System.err.println("Error getting future result: " + e.getMessage()); + } + + // Bad: Forgetting to handle exceptions within the CompletableFuture chain + CompletableFuture errorFuture = CompletableFuture.supplyAsync(() -> { + if (true) throw new RuntimeException("Simulated async error!"); + return "Won't reach here"; + }).thenApply(s -> s.toUpperCase()); // This stage might not run, or exception propagates + + try { + errorFuture.join(); // Will throw CompletionException here if not handled by .exceptionally() + } catch (Exception e) { + // assertThat(e.getCause()).isInstanceOf(RuntimeException.class).hasMessage("Processing failed"); + System.out.println("Error from errorFuture: " + e.getClass().getName() + ": "+ e.getCause().getMessage()); + } + // An .exceptionally() or .handle() should be used in the chain. + } +}]]> + + + + + + + Module System (Java 9+) + Leverage the Java Platform Module System (JPMS) for Strong Encapsulation + + + For applications built on Java 9 or later, consider designing them as modules to achieve strong encapsulation and reliable configuration. Create a `module-info.java` file at the root of your source code to declare your module. Use `requires` clauses to specify dependencies, `exports` clauses to make packages publicly available, `opens` clauses for reflection access, and `provides ... with ...` for service discovery. Carefully consider which packages to export to maintain good encapsulation. + + + + + + + + + + + + + + Performance Considerations with Modern Features + Be Mindful of Performance Implications of Modern Java Features + + + Always profile your application before attempting optimizations. Avoid premature optimization. Choose appropriate data structures for the task. Streams can sometimes have overhead compared to simple loops for very small collections. Parallel streams can improve performance for CPU-bound tasks on large datasets but can degrade performance if misused. `Optional` can add object allocation overhead. Lazy initialization should be implemented correctly using double-checked locking or suppliers. + + + + numbers = new ArrayList<>(LIST_SIZE); + for(int i=0; i (long)i*i).sum(); // Squaring, CPU intensive + long endTime = System.nanoTime(); + System.out.println("\nSequential sum of squares: " + sumSequential + " in " + (endTime-startTime)/1_000_000 + "ms"); + + startTime = System.nanoTime(); + long sumParallel = numbers.parallelStream().mapToLong(i -> (long)i*i).sum(); + endTime = System.nanoTime(); + System.out.println("Parallel sum of squares: " + sumParallel + " in " + (endTime-startTime)/1_000_000 + "ms"); + // On multi-core machines, parallel *may* be faster here. + } +}]]> + + + 0.8) costlyObject.use(); + System.out.println("Costly object would have been initialized in constructor if uncommented."); + } + + public static void main(String[] args) { + PerformanceAntiPattern pap = new PerformanceAntiPattern(); + pap.sometimesUseCostlyObject(); + + List smallList = List.of(1, 2, 3, 4, 5); + // Bad: Using parallel stream for a tiny list and simple operation. + // Overhead of parallelization likely exceeds any benefit. + System.out.println("\nCalculating sum of small list (parallel, likely inefficient):"); + long sum = smallList.parallelStream().mapToInt(Integer::intValue).sum(); + System.out.println("Sum: " + sum); + } +}]]> + + + + + + + Testing Modern Java Code + Adapt Testing Strategies for Modern Java Features + + + Adapt testing strategies to properly test modern Java features. Focus on testing behavior rather than implementation when testing lambda expressions and functional code. Use appropriate mocking strategies for functional interfaces and streams. Test asynchronous code with CompletableFuture properly, including timeout scenarios and exception handling. Ensure proper testing coverage of Optional usage patterns and edge cases. Utilize modern testing frameworks and assertion libraries that integrate well with modern Java features. + + + + filterAndToUpper(List input, String startsWith) { + if (Objects.isNull(input)) return List.of(); + return input.stream() + .filter(s -> s != null && s.startsWith(startsWith)) + .map(String::toUpperCase) + .collect(Collectors.toList()); + } + + public Optional findFirstLongString(List input, int minLength) { + if (Objects.isNull(input)) return Optional.empty(); + return input.stream() + .filter(s -> s != null && s.length() >= minLength) + .findFirst(); + } + + public CompletableFuture processDataAsync(String data) { + return CompletableFuture.supplyAsync(() -> { + try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } + if (data.equals("fail")) throw new RuntimeException("Processing failed"); + return "PROCESSED:" + data; + }); + } +} + +// Conceptual Tests (using System.out for assertions for simplicity here) +public class TestingModernCodeExample { + + // @Test // Example with AssertJ style (if library was present) + void testFilterAndToUpper_withAssertJ() { + ModernFeaturesService service = new ModernFeaturesService(); + List input = List.of("apple", "apricot", "banana", "avocado"); + List result = service.filterAndToUpper(input, "ap"); + // assertThat(result).containsExactlyInAnyOrder("APPLE", "APRICOT"); + System.out.println("testFilterAndToUpper: " + result); // Expected: APPLE, APRICOT + } + + // @Test + void testFindFirstLongString_found_withAssertJ() { + ModernFeaturesService service = new ModernFeaturesService(); + List input = List.of("short", "verylongstring", "medium"); + Optional result = service.findFirstLongString(input, 10); + // assertThat(result).isPresent().hasValue("verylongstring"); + System.out.println("testFindFirstLongString (found): " + result); // Expected: Optionalverylongstring + } + + // @Test + void testFindFirstLongString_notFound_withAssertJ() { + ModernFeaturesService service = new ModernFeaturesService(); + List input = List.of("short", "medium"); + Optional result = service.findFirstLongString(input, 10); + // assertThat(result).isEmpty(); + System.out.println("testFindFirstLongString (not found): " + result); // Expected: Optional.empty + } + + // @Test + void testProcessDataAsync_success() { + ModernFeaturesService service = new ModernFeaturesService(); + CompletableFuture future = service.processDataAsync("test"); + // In a real test, use Awaitility or future.join() with try-catch for CompletionException + try { + String result = future.get(); // Blocking for example, use non-blocking in real tests + // assertThat(result).isEqualTo("PROCESSED:test"); + System.out.println("testProcessDataAsync (success): " + result); // Expected: PROCESSED:test + } catch (Exception e) { e.printStackTrace(); } + } + + // @Test + void testProcessDataAsync_failure() { + ModernFeaturesService service = new ModernFeaturesService(); + CompletableFuture future = service.processDataAsync("fail"); + // assertThatThrownBy(future::join).isInstanceOf(CompletionException.class); + try { + future.join(); // This will throw CompletionException + } catch (Exception e) { + // assertThat(e.getCause()).isInstanceOf(RuntimeException.class).hasMessage("Processing failed"); + System.out.println("testProcessDataAsync (failure expected): " + e.getClass().getName() + " -> " + e.getCause().getMessage()); + } + } + + public static void main(String[] args) { + System.out.println("These are conceptual tests. Use a testing framework for real scenarios."); + TestingModernCodeExample tests = new TestingModernCodeExample(); + tests.testFilterAndToUpper_withAssertJ(); + tests.testFindFirstLongString_found_withAssertJ(); + tests.testFindFirstLongString_notFound_withAssertJ(); + tests.testProcessDataAsync_success(); + tests.testProcessDataAsync_failure(); + } +}]]> + + + processList(List data) { + // Complex logic that should be thoroughly tested + return data.stream() + .filter(s -> s.length() > 5) + .map(s -> s.substring(0, 5)) + .findFirst(); + } + + public static void main(String[] args) { + InsufficientTesting it = new InsufficientTesting(); + // Only testing the "happy path" + Optional result = it.processList(List.of("longstring", "another")); + System.out.println("Result (happy path only): " + result.orElse("NOTHING")); + + // Not tested: + // - Empty list input + // - List with no strings matching filter + // - List with nulls (if stream pipeline doesn't handle them) + // - Performance with very large lists + // This lack of thorough testing can lead to bugs in production. + } +}]]> + + + + + + + Use Text Blocks for Readable Multi-line Strings + Employ Text Blocks for Clear and Maintainable Multi-line String Literals + + + Utilize text blocks (`"""..."""`) to represent multi-line string literals in a way that preserves indentation and formatting, making them easier to read and write, especially for embedded code snippets like JSON, XML, SQL, or HTML. Text blocks automatically handle newline characters and manage indentation. Incidental leading white space is automatically stripped from each line. You can control trailing white space and use escape sequences as needed. + + + + + +

Hello, Java Text Blocks!

+ + + """; + System.out.println("HTML:\n" + html); + + // Good: JSON snippet with preserved indentation + String json = """ + { + "name": "Java Text Block", + "feature": "Multi-line strings", + "since": 15 + } + """; + System.out.println("JSON:\n" + json); + + // Good: SQL query + String query = """ + SELECT id, name, email + FROM users + WHERE department = 'Engineering' + ORDER BY name; + """; + System.out.println("SQL Query:\n" + query); + + // Good: Controlling indentation - the content's indentation relative + // to the closing """ is preserved. + String indented = """ + Line 1 + Line 2 (more indented) + Line 3 (less indented than line 2, but aligned with Line 1 relative to closing quotes) + """; + System.out.println("Indented Example:\n" + indented); + } +}]]>
+
+ + \n" + + " \n" + + "

Hello, old Java strings!

\n" + + " \n" + + "\n"; + System.out.println("Old HTML:\n" + htmlOld); + + // Bad: Hard to read and maintain SQL query + String queryOld = "SELECT id, name, email\n" + + "FROM users\n" + + "WHERE department = 'Engineering'\n" + + "ORDER BY name;\n"; + System.out.println("Old SQL Query:\n" + queryOld); + + // Bad: Incorrectly trying to manage indentation within a text block + // by having significant content on the same line as opening """ + // (This can work, but it's less clear than starting content on new line) + String mixedStyle = """ This is the first line. + This is the second line. + """; // The indentation is determined by the least indented line or the closing """ + System.out.println("Mixed Style (potentially confusing indentation):\n" + mixedStyle); + } +}]]>
+
+
+
+ + diff --git a/spml/src/main/resources/142-java-functional-programming.xml b/spml/src/main/resources/142-java-functional-programming.xml new file mode 100644 index 00000000..a35979c0 --- /dev/null +++ b/spml/src/main/resources/142-java-functional-programming.xml @@ -0,0 +1,643 @@ + + + + + + false + + java + functional-programming + immutability + streams + lambda + + + +
+ Java Functional Programming rules +
+ + + You are a Senior software engineer with extensive experience in Java software development + + + Java functional programming revolves around immutable objects and state transformations, ensuring functions are pure (no side effects, depend only on inputs). It leverages functional interfaces, concise lambda expressions, and the Stream API for collection processing. Core paradigms include function composition, `Optional` for null safety, and higher-order functions. Modern Java features like Records enhance immutable data transfer, while pattern matching (for `instanceof` and `switch`) and switch expressions improve conditional logic. Sealed classes and interfaces enable controlled, exhaustive hierarchies, and upcoming Stream Gatherers will offer advanced custom stream operations. + + + + + + + Immutable Objects + Ensure Objects are Immutable + + +- Use `final` classes and fields. +- Initialize all fields in the constructor. +- Do not provide setter methods. +- Return defensive copies of mutable fields (e.g., collections, dates) when exposing them via getters. + + + + +import java.util.List; +import java.util.ArrayList; + +public final class Person { + private final String name; + private final int age; + private final List<String> hobbies; // Make it List, not ArrayList + + public Person(String name, int age, List<String> hobbies) { + this.name = name; + this.age = age; + // Ensure the incoming list is defensively copied to an immutable list + this.hobbies = List.copyOf(hobbies); + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + // Return an immutable view or a defensive copy + public List<String> getHobbies() { + return this.hobbies; // List.copyOf already returns an unmodifiable list + } +} + + + + + + + + State Immutability + Prefer Immutable State Transformations + + +- Instead of modifying existing objects, return new objects representing the new state. +- Utilize collectors that produce immutable collections (e.g., `Collectors.toUnmodifiableList()`). +- Leverage immutable collection types provided by libraries or Java itself. + + + + +import java.util.List; +import java.util.stream.Collectors; + +public class PriceCalculator { + public static List<Double> applyDiscount(List<Double> prices, double discount) { + return prices.stream() + .map(price -> price * (1 - discount)) + .collect(Collectors.toUnmodifiableList()); // Ensures the returned list is immutable + } +} + + + + + + + + Pure Functions + Write Pure Functions + + +- Functions should depend only on their input parameters and not on any external or hidden state. +- They should not cause any side effects (e.g., modifying external variables, I/O operations). +- Given the same input, a pure function must always return the same output. +- Avoid modifying external state or relying on it. + + + + +import java.util.List; +import java.util.stream.Collectors; + +public class MathOperations { + // Pure function: depends only on input, no side effects + public static int add(int a, int b) { + return a + b; + } + + // Pure function: transforms input list to a new list without modifying the original + public static List<Integer> doubleNumbers(List<Integer> numbers) { + return numbers.stream() + .map(n -> n * 2) + .collect(Collectors.toList()); // Could also be toUnmodifiableList() + } +} + + + + + + + + Functional Interfaces + Utilize Functional Interfaces Effectively + + +- Prefer built-in functional interfaces from `java.util.function` (e.g., `Function`, `Predicate`, `Consumer`, `Supplier`, `UnaryOperator`) when they suit the need. +- Create custom functional interfaces (annotated with `@FunctionalInterface`) for specific, clearly defined single abstract methods. +- Keep functional interfaces focused on a single responsibility. + + + + +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.time.LocalDateTime; + +// Built-in functional interfaces +class FunctionalInterfaceExamples { + Function<String, Integer> stringToLength = String::length; + Predicate<Integer> isEven = n -> n % 2 == 0; + Consumer<String> printer = System.out::println; + Supplier<LocalDateTime> now = LocalDateTime::now; +} + +// Custom functional interface +@FunctionalInterface +interface Validator<T> { + boolean validate(T value); +} + + + + + + + + Lambda Expressions + Employ Lambda Expressions Clearly and Concisely + + +- Use method references (e.g., `String::length`, `System.out::println`) when they are clearer and more concise than an equivalent lambda expression. +- Keep lambda expressions short and focused on a single piece of logic to maintain readability. +- Extract complex or multi-line lambda logic into separate, well-named private methods. + + + + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class LambdaExamples { + public static void main(String[] args) { + List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve"); + + // Method reference for conciseness + names.forEach(System.out::println); + + // Simple, readable lambda + List<String> longNames = names.stream() + .filter(name -> name.length() > 4) + .collect(Collectors.toList()); + + // Complex logic extracted to a private helper method + List<String> validNames = names.stream() + .filter(LambdaExamples::isValidName) + .collect(Collectors.toList()); + + System.out.println("Long names: " + longNames); + System.out.println("Valid names: " + validNames); + } + + // Helper method for more complex lambda logic + private static boolean isValidName(String name) { + return name.length() > 3 && Character.isUpperCase(name.charAt(0)); + } +} + + + + + + + + Streams + Leverage Streams for Collection Processing + + +- Use the Stream API for processing sequences of elements from collections or other sources. +- Chain stream operations (intermediate operations like `filter`, `map`, `sorted`) to create a pipeline for complex transformations. +- Consider using parallel streams (`collection.parallelStream()`) for potentially improved performance on large datasets, but be mindful of the overhead and suitability for the task. +- Choose appropriate terminal operations (e.g., `collect`, `forEach`, `reduce`, `findFirst`, `anyMatch`) to produce a result or side-effect. + + + + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class StreamExamples { + public static void main(String[] args) { + List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + // Basic stream operations: filter even numbers and square them + List<Integer> evenSquares = numbers.stream() + .filter(n -> n % 2 == 0) + .map(n -> n * n) + .collect(Collectors.toList()); + System.out.println("Even squares: " + evenSquares); + + // Advanced stream operations: partitioning numbers + Map<Boolean, List<Integer>> partitionedByGreaterThanFive = numbers.stream() + .collect(Collectors.partitioningBy(n -> n > 5)); + System.out.println("Partitioned by > 5: " + partitionedByGreaterThanFive); + + // Parallel stream for calculating average (use with caution, consider dataset size) + double average = numbers.parallelStream() + .mapToDouble(Integer::doubleValue) + .average() + .orElse(0.0); + System.out.println("Average: " + average); + } +} + + + + + + + + Functional Programming Paradigms + Apply Core Functional Programming Paradigms + + +- **Function Composition**: Combine simpler functions to create more complex ones. Use `Function.compose()` and `Function.andThen()`. +- **Optional for Null Safety**: Use `Optional<T>` to represent values that may be absent, avoiding `NullPointerExceptions` and clearly signaling optionality. +- **Recursion**: Implement algorithms using recursion where it naturally fits the problem (e.g., tree traversal), especially tail recursion if supported or optimized by the JVM. +- **Higher-Order Functions**: Utilize functions that accept other functions as arguments or return them as results (e.g., `Stream.map`, `Stream.filter`). + + + + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.IntStream; + +public class FunctionalParadigms { + + // Function composition + public static void demonstrateComposition() { + Function<Integer, String> intToString = Object::toString; + Function<String, Integer> stringLength = String::length; + // Executes intToString first, then stringLength + Function<Integer, Integer> composedLengthAfterToString = stringLength.compose(intToString); + System.out.println("Composed (123 -> length): " + composedLengthAfterToString.apply(123)); // Output: 3 + } + + // Optional usage for safe division + public static Optional<Double> divideNumbers(Double numerator, Double denominator) { + if (Objects.isNull(denominator) || denominator == 0) { + return Optional.empty(); + } + return Optional.of(numerator / denominator); + } + + // Factorial using IntStream (more functional and often safer for large n) + public static long factorialFunctional(int n) { + if (n < 0) throw new IllegalArgumentException("Factorial not defined for negative numbers"); + return IntStream.rangeClosed(1, n) + .asLongStream() // Ensure long for intermediate products + .reduce(1L, (a, b) -> a * b); + } + + // Recursion example: factorial (iterative version often preferred for stack safety in Java) + // Note: Streams provide a more functional way for such operations in many cases. + public static long factorialRecursive(int n) { + if (n < 0) throw new IllegalArgumentException("Factorial not defined for negative numbers"); + if (n == 0 || n == 1) return 1; + return n * factorialRecursive(n - 1); + } + + // Higher-order function: memoization + public static <T, R> Function<T, R> memoize(Function<T, R> function) { + Map<T, R> cache = new ConcurrentHashMap<>(); + // The returned function closes over the cache + return input -> cache.computeIfAbsent(input, function); + } + + public static void main(String[] args) { + demonstrateComposition(); + + System.out.println("Divide 10 by 2: " + divideNumbers(10.0, 2.0).orElse(Double.NaN)); + System.out.println("Divide 10 by 0: " + divideNumbers(10.0, 0.0).orElse(Double.NaN)); + + System.out.println("Factorial recursive (5): " + factorialRecursive(5)); + System.out.println("Factorial functional (5): " + factorialFunctional(5)); + + Function<Integer, Integer> expensiveOperation = x -> { + System.out.println("Computing for " + x); + try { Thread.sleep(1000); } catch (InterruptedException e) {} + return x * x; + }; + + Function<Integer, Integer> memoizedOp = memoize(expensiveOperation); + System.out.println("Memoized (4): " + memoizedOp.apply(4)); // Computes + System.out.println("Memoized (4): " + memoizedOp.apply(4)); // Returns from cache + System.out.println("Memoized (5): " + memoizedOp.apply(5)); // Computes + } +} + + + + + + + + Leverage Records for Immutable Data Transfer + Leverage Records for Immutable Data Transfer + + +- Use Records (JEP 395, standardized in Java 16) as the primary way to model simple, immutable data aggregates. +- Records automatically provide constructors, getters (accessor methods with the same name as the field), `equals()`, `hashCode()`, and `toString()` methods, reducing boilerplate. +- This aligns perfectly with the functional paradigm's preference for immutability and conciseness. + + + + +public record PointRecord(int x, int y) { + // Optional: add custom compact constructors, static factory methods, or instance methods. + // By default, all fields are final, and public accessor methods (e.g., x(), y()) are generated. +} + +// Usage: +// PointRecord p = new PointRecord(10, 20); +// int xVal = p.x(); // Accessor method +// int yVal = p.y(); // Accessor method + + + + +public final class PointClass { + private final int x; + private final int y; + + public PointClass(int x, int y) { + this.x = x; + this.y = y; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (Objects.isNull(o) || getClass() != o.getClass()) return false; + PointClass that = (PointClass) o; + return x == that.x && y == that.y; + } + + @Override + public int hashCode() { + return java.util.Objects.hash(x, y); + } + + @Override + public String toString() { + return "PointClass[" + + "x=" + x + ", " + + "y=" + y + ']'; + } +} + + + + + + + + Employ Pattern Matching for `instanceof` and `switch` + Employ Pattern Matching for Type-Safe Conditional Logic + + +- Utilize Pattern Matching for `instanceof` to simplify type checks and casts in a single step. +- Employ Pattern Matching for `switch` for more expressive and robust conditional logic, especially with sealed types and records. +- This reduces boilerplate, improves readability, and enhances type safety. + + + + +public String processShapeWithPatternInstanceof(Object shape) { + if (shape instanceof Circle c) { // Type test and binding in one + return "Circle with radius " + c.getRadius(); + } else if (shape instanceof Rectangle r) { + return "Rectangle with width " + r.getWidth() + " and height " + r.getHeight(); + } + return "Unknown shape"; +} + +// Pattern Matching for switch with Records and Sealed Interfaces +sealed interface Shape permits CircleRecord, RectangleRecord, SquareRecord {} +record CircleRecord(double radius) implements Shape {} +record RectangleRecord(double length, double width) implements Shape {} +record SquareRecord(double side) implements Shape {} + +public String processShapeWithPatternSwitch(Shape shape) { + return switch (shape) { + case CircleRecord c -> "Circle with radius " + c.radius(); + case RectangleRecord r -> "Rectangle with length " + r.length() + " and width " + r.width(); + case SquareRecord s -> "Square with side " + s.side(); + // No default needed if all permitted types of the sealed interface are covered + }; +} + + + + +public String processShapeLegacy(Object shape) { + if (shape instanceof Circle) { + Circle c = (Circle) shape; + return "Circle with radius " + c.getRadius(); + } else if (shape instanceof Rectangle) { + Rectangle r = (Rectangle) shape; + return "Rectangle with width " + r.getWidth() + " and height " + r.getHeight(); + } + return "Unknown shape"; +} + +// Assume Circle and Rectangle classes exist for this example +// class Circle { public double getRadius() { return 0; } } +// class Rectangle { public double getWidth() { return 0; } public double getHeight() { return 0; } } + + + + + + + + Use Switch Expressions for Concise Multi-way Conditionals + Use Switch Expressions for Concise and Safe Multi-way Conditionals + + +- Prefer Switch Expressions (JEP 361, Java 14) over traditional switch statements for assigning the result of a multi-way conditional to a variable. +- Switch expressions are more concise, less error-prone (e.g., no fall-through by default, compiler checks for exhaustiveness with enums/sealed types). +- They fit well with functional programming's emphasis on expressions over statements. + + + + +public String getDayTypeWithSwitchExpr(String day) { + return switch (day) { + case "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY" -> "Weekday"; + case "SATURDAY", "SUNDAY" -> "Weekend"; + default -> throw new IllegalArgumentException("Invalid day: " + day); + }; +} + +// Example with enum for exhaustive switch +enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY } + +public String getDayCategory(Day day) { + return switch (day) { + case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Weekday"; + case SATURDAY, SUNDAY -> "Weekend"; + // No default needed if all enum constants are covered + }; +} + + + + +public String getDayTypeLegacy(String day) { + String type; + switch (day) { + case "MONDAY": + case "TUESDAY": + case "WEDNESDAY": + case "THURSDAY": + case "FRIDAY": + type = "Weekday"; + break; + case "SATURDAY": + case "SUNDAY": + type = "Weekend"; + break; + default: + throw new IllegalArgumentException("Invalid day: " + day); + } + return type; +} + + + + + + + + Leverage Sealed Classes and Interfaces for Controlled Hierarchies + Leverage Sealed Classes and Interfaces for Precise Domain Modeling + + +- Use Sealed Classes and Interfaces (JEP 409, Java 17) to define class/interface hierarchies where all direct subtypes are known, finite, and explicitly listed. +- This enables more robust domain modeling and allows the compiler to perform exhaustive checks in pattern matching (e.g., with `switch` expressions), eliminating the need for a default case in many scenarios. +- Particularly useful for creating sum types (algebraic data types) which are common in functional programming. + + + + +// Define a sealed interface for different types of events +public sealed interface Event permits LoginEvent, LogoutEvent, FileUploadEvent { + long getTimestamp(); +} + +// Define permitted implementations (often records for immutability) +public record LoginEvent(String userId, long timestamp) implements Event { + @Override public long getTimestamp() { return timestamp; } +} + +public record LogoutEvent(String userId, long timestamp) implements Event { + @Override public long getTimestamp() { return timestamp; } +} + +public record FileUploadEvent(String userId, String fileName, long fileSize, long timestamp) implements Event { + @Override public long getTimestamp() { return timestamp; } +} + +// A function processing the sealed hierarchy can be made exhaustive +public class EventProcessor { + public String processEvent(Event event) { + return switch (event) { + case LoginEvent le -> "User " + le.userId() + " logged in at " + le.getTimestamp(); + case LogoutEvent loe -> "User " + loe.userId() + " logged out at " + loe.getTimestamp(); + case FileUploadEvent fue -> "User " + fue.userId() + " uploaded " + fue.fileName() + " at " + fue.getTimestamp(); + // No default case is necessary if the switch is exhaustive for all permitted types of Event. + }; + } +} + + + + + + + + Explore Stream Gatherers for Custom Stream Operations + Explore Stream Gatherers for Advanced Custom Stream Operations + + +- For complex or highly custom stream processing tasks that are not easily achieved with standard terminal operations or collectors, investigate Stream Gatherers (JEP 461). +- Gatherers (`java.util.stream.Gatherer`) allow defining custom intermediate operations, offering more flexibility and power for sophisticated data transformations within functional pipelines. +- This feature is aimed at more advanced use cases where reusability and composition of stream operations are key. + + + + +import java.util.List; +import java.util.stream.Stream; +// import java.util.stream.Gatherers; // Assuming this is where predefined gatherers might reside + +public class StreamGathererExample { + + // Hypothetical: A custom gatherer that creates sliding windows of elements. + // The actual implementation of such a gatherer would be more involved. + // static <T> Gatherer<T, ?, List<T>> windowed(int size) { + // // ... implementation details ... + // return null; // Placeholder + // } + + public static void main(String[] args) { + // List<List<Integer>> windows = Stream.of(1, 2, 3, 4, 5, 6, 7) + // .gather(windowed(3)) // Using a hypothetical custom 'windowed' gatherer + // .toList(); + // + // // Expected output might be: [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6], [5, 6, 7]] + // System.out.println(windows); + + System.out.println("Stream Gatherers are a new feature. Refer to official Java documentation for concrete examples and API details as they become available."); + } +} + +// Rule of Thumb: +// Before implementing very complex custom collectors or resorting to imperative loops for intricate stream transformations, +// evaluate if a Stream Gatherer could offer a more declarative, reusable, and composable solution. +// This is for advanced stream users looking to build sophisticated data processing pipelines. + + + + + +
diff --git a/spml/src/main/resources/143-java-data-oriented-programming.xml b/spml/src/main/resources/143-java-data-oriented-programming.xml new file mode 100644 index 00000000..52a8a47e --- /dev/null +++ b/spml/src/main/resources/143-java-data-oriented-programming.xml @@ -0,0 +1,921 @@ + + + + + + false + + java + data-oriented-programming + immutability + records + pure-functions + best-practices + + 1.0.0 + + +
+ Java rules to apply data oriented programming style +
+ + + You are a Senior software engineer with extensive experience in Java software development + + + + Java Data-Oriented Programming emphasizes separating code (behavior) from data structures, which should ideally be immutable (e.g., using records). Data manipulation should occur via pure functions that transform data into new instances. It's often beneficial to keep data structures flat and denormalized (using IDs for references) where appropriate, and to start with generic data representations (like `Map<String, Object>`) converting to specific types only when necessary. Data integrity is ensured through pure validation functions. Flexible, generic data access layers facilitate working with various data types and storage mechanisms. All data transformations should be explicit, traceable, and composed of clear, pure functional steps. + + + + + + + + Separate Code from Data + Decouple Behavior (Code) from Data Structures + + +- Use records or simple POJOs primarily for holding data. +- Place behavior (methods that operate on data) in separate utility classes or services. +- Avoid mixing state (fields) and complex behavior (methods with logic) within the same class intended as a data carrier. +- Prefer static methods in utility classes for operations on data objects. +- Design data structures to be self-contained and focused solely on representing state. + + + + + + + + + + + + + + Data Should Be Immutable + Ensure Data Immutability + + +- Use records (which are inherently immutable) whenever possible for data carriers. +- Declare all fields as `final`. +- Do not provide setter methods for fields. +- When returning collections or other mutable types from getters, return defensive copies or unmodifiable views (e.g., `List.copyOf()`, `Collections.unmodifiableList()`). +- For transformations, always create and return new instances with the modified data rather than altering existing instances. + + + + features) { + // Canonical constructor is implicitly final for fields + // Records provide public accessors (host(), port(), features()) + // equals(), hashCode(), toString() are auto-generated + + // For mutable collections in constructor, ensure immutability + public ServerConfig(String host, int port, List features) { + this.host = host; + this.port = port; + this.features = List.copyOf(features); // Create an immutable copy + } + + // Getter for list returns the immutable copy + @Override + public List features() { + return this.features; // Already an immutable list from List.copyOf + } +} + +class ImmutabilityExample { + public static void main(String args) { + List initialFeatures = new ArrayList<>(); + initialFeatures.add("FeatureA"); + + ServerConfig config1 = new ServerConfig("localhost", 8080, initialFeatures); + System.out.println("Config1: " + config1); + + initialFeatures.add("FeatureB"); // Modify original list + System.out.println("Config1 after modifying original list: " + config1); // config1.features is unaffected + + try { + config1.features().add("FeatureC"); // Should throw UnsupportedOperationException + } catch (UnsupportedOperationException e) { + System.out.println("Attempt to modify config1.features(): " + e.getMessage()); + } + + // Transformation creates a new instance + ServerConfig config2 = new ServerConfig(config1.host(), 9090, config1.features()); + System.out.println("Config2 (new port): " + config2); + System.out.println("Config1 is unchanged: " + config1); + } +}]]> + + + features; // Mutable list + + public MutableConfig(String host, int port, List features) { + this.host = host; + this.port = port; + this.features = new ArrayList<>(features); // Stores a mutable copy + } + + public void setHost(String host) { this.host = host; } + public void setPort(int port) { this.port = port; } + public void addFeature(String feature) { this.features.add(feature); } // Modifies internal state + + public List getFeatures() { + return this.features; // Returns a reference to the internal mutable list + } + + @Override + public String toString() { + return "MutableConfig{host='" + host + "', port=" + port + ", features=" + features + "}"; + } + + public static void main(String args) { + List initialFeatures = new ArrayList<>(); + initialFeatures.add("InitialFeature"); + MutableConfig mConfig = new MutableConfig("server1", 80, initialFeatures); + System.out.println("mConfig: " + mConfig); + + mConfig.setPort(8080); // State is mutated + mConfig.addFeature("NewFeature"); // State is mutated + System.out.println("mConfig mutated: " + mConfig); + + List gotFeatures = mConfig.getFeatures(); + gotFeatures.add("AnotherFeatureAddedExternally"); // External modification of internal state! + System.out.println("mConfig after external list modification: " + mConfig); + } +}]]> + + + + + + + Use Pure Functions to Manipulate Data + Employ Pure Functions for Data Transformations + + +- Functions that manipulate data should depend solely on their input parameters, not on any instance or external state. +- They must not cause any side effects (e.g., modifying input objects, global variables, I/O operations). +- Pure functions are inherently stateless: given the same input, they always produce the same output. +- Transformations should return new data instances rather than modifying the input data directly. +- Prefer static methods for such pure data transformation functions. +- Keep each function focused on a single, well-defined transformation. + + + + applyDiscountToItems(List items, double discountPercentage) { + if (discountPercentage < 0 || discountPercentage > 1) { + throw new IllegalArgumentException("Discount must be between 0 and 1."); + } + return items.stream() + .map(item -> new ItemPrice(item.itemId(), item.price() * (1 - discountPercentage))) + .collect(Collectors.toUnmodifiableList()); // Returns a new, immutable list + } +} + +class PureFunctionExample { + public static void main(String args) { + double itemPrice = 100.0; + double tax = 0.07; + double total = PriceOperations.calculateTotalWithTax(itemPrice, tax); + System.out.println("Total for price " + itemPrice + " with tax " + tax + ": " + total); + + List catalogue = List.of( + new ItemPrice("A001", 50.0), + new ItemPrice("B002", 120.0) + ); + List discountedCatalogue = PriceOperations.applyDiscountToItems(catalogue, 0.10); // 10% discount + System.out.println("Original catalogue: " + catalogue); + System.out.println("Discounted catalogue: " + discountedCatalogue); + } +}]]> + + + prices; // Modifies this list + + public PriceCalculator(double taxRate, List initialPrices) { + this.taxRate = taxRate; + this.prices = new ArrayList<>(initialPrices); // Stores mutable state + } + + // Depends on instance state (taxRate) + public double calculateTotalForItem(double price) { + return price * (1 + this.taxRate); + } + + // Modifies instance state (this.prices) - a side effect + public void applyDiscountToAllItems(double discount) { + for (int i = 0; i < this.prices.size(); i++) { + this.prices.set(i, this.prices.get(i) * (1 - discount)); + } + } + + public List getPrices() { + return this.prices; + } + + public static void main(String args) { + PriceCalculator calc = new PriceCalculator(0.05, List.of(10.0, 20.0)); + System.out.println("Total for 10.0: " + calc.calculateTotalForItem(10.0)); // Depends on calc.taxRate + + System.out.println("Prices before discount: " + calc.getPrices()); + calc.applyDiscountToAllItems(0.1); + System.out.println("Prices after discount (mutated): " + calc.getPrices()); // Original list inside calc is modified + } +}]]> + + + + + + + Keep Data Flat and Denormalized + Prefer Flatter Data Structures with References + + +- Avoid excessively deep nesting of data objects, which can make data harder to access and manage. +- When representing relationships, consider using references (like IDs) instead of directly embedding complex objects within other objects, especially for many-to-many or one-to-many relationships. +- Store related but distinct entities in separate flat collections or maps, linked by these IDs. +- This approach can simplify querying, updating, and serializing data. +- However, balance this with the need for data co-location for performance in specific access patterns. Denormalization is a trade-off. + + + + taskIds) {} +record EmployeeData(String id, String name, String departmentId, List projectIds) {} +record DepartmentData(String id, String name, List employeeIds) {} + +class FlatDataStore { + private final Map tasks = new HashMap<>(); + private final Map projects = new HashMap<>(); + private final Map employees = new HashMap<>(); + private final Map departments = new HashMap<>(); + + // Methods to add and retrieve data (simplified) + public void addTask(TaskData task) { tasks.put(task.id(), task); } + public TaskData getTask(String id) { return tasks.get(id); } + // ... similar methods for projects, employees, departments + + public List getTasksForProject(String projectId) { + ProjectData project = projects.get(projectId); + if (Objects.isNull(project)) return List.of(); + return project.taskIds().stream() + .map(tasks::get) + .collect(Collectors.toList()); + } + public static void main(String args) { + FlatDataStore store = new FlatDataStore(); + store.addTask(new TaskData("T1", "Design UI", "...", "E1")); + // ... add more data + // Accessing data involves looking up by ID and potentially joining. + } +}]]> + + + employees; + public Department(String name) { this.departmentName = name; this.employees = new ArrayList<>(); } + public void addEmployee(Employee e) { this.employees.add(e); } + public List getEmployees() { return employees; } +} + +class Employee { + private String employeeName; + private List projects; + public Employee(String name) { this.employeeName = name; this.projects = new ArrayList<>(); } + public void addProject(Project p) { this.projects.add(p); } + public List getProjects() { return projects; } +} + +class Project { + private String projectName; + private List tasks; + public Project(String name) { this.projectName = name; this.tasks = new ArrayList<>(); } + public void addTask(Task t) { this.tasks.add(t); } + public List getTasks() { return tasks; } +} + +class Task { + private String taskName; + public Task(String name) { this.taskName = name; } + public String getTaskName() { return taskName; } +} + +class DeeplyNestedExample { + public static void main(String args) { + Department dept = new Department("Engineering"); + Employee emp = new Employee("Alice"); + Project proj = new Project("New Platform"); + Task task1 = new Task("Define API"); + + proj.addTask(task1); + emp.addProject(proj); + dept.addEmployee(emp); + // Accessing task1 requires: dept.getEmployees().get(0).getProjects().get(0).getTasks().get(0) + // Updates are complex. Serialization can be very large. + System.out.println(dept.getEmployees().get(0).getProjects().get(0).getTasks().get(0).getTaskName()); + } +}]]> + + + + + + + Keep Data Generic Until Specific + Start with Generic Data Structures, Convert to Specific Types When Needed + + +- For highly dynamic or externally defined data, start with generic data structures like `Map<String, Object>` or JSON-like trees. +- This allows flexibility in handling varied or evolving data schemas. +- Convert to specific, strongly-typed objects (like records) only when the data needs to be processed in a type-safe manner or when specific business logic applies. +- Implement robust, type-safe converter functions or classes to perform this transformation. +- Validate data during the conversion process to ensure it conforms to the expected specific type. +- Clearly document the expected structure of the generic data. + + + + attributes) {} + +// Converter class +class UserProfileConverter { + public static UserProfile toUserProfile(GenericData genericData) { + Map attrs = genericData.attributes(); + + // Basic validation and type casting (could be more robust) + String email = (String) attrs.get("email"); + String firstName = (String) attrs.get("firstName"); + String lastName = (String) attrs.get("lastName"); + LocalDate birthDate = null; + Object bd = attrs.get("birthDate"); + if (bd instanceof String) { + birthDate = LocalDate.parse((String) bd); + } else if (bd instanceof LocalDate) { + birthDate = (LocalDate) bd; + } else if (bd != null) { + throw new IllegalArgumentException("Invalid birthDate format"); + } + + if (Objects.isNull(email) || Objects.isNull(firstName) || Objects.isNull(lastName) || Objects.isNull(birthDate)) { + throw new IllegalArgumentException("Missing required user profile fields"); + } + + return new UserProfile(email, firstName, lastName, birthDate); + } + + public static GenericData fromUserProfile(UserProfile userProfile) { + Map attrs = new HashMap<>(); + attrs.put("email", userProfile.email()); + attrs.put("firstName", userProfile.firstName()); + attrs.put("lastName", userProfile.lastName()); + attrs.put("birthDate", userProfile.birthDate().toString()); + return new GenericData(attrs); + } +} + +class GenericDataExample { + public static void main(String args) { + Map rawData = new HashMap<>(); + rawData.put("email", "alice@example.com"); + rawData.put("firstName", "Alice"); + rawData.put("lastName", "Wonder"); + rawData.put("birthDate", "1990-07-15"); + rawData.put("extraField", "someValue"); // Generic data can have extra fields + + GenericData genericUser = new GenericData(rawData); + System.out.println("Generic user data: " + genericUser.attributes()); + + try { + UserProfile specificProfile = UserProfileConverter.toUserProfile(genericUser); + System.out.println("Converted to specific UserProfile: " + specificProfile); + + GenericData backToGeneric = UserProfileConverter.fromUserProfile(specificProfile); + System.out.println("Converted back to generic: " + backToGeneric.attributes()); + + } catch (IllegalArgumentException e) { + System.err.println("Conversion error: " + e.getMessage()); + } + } +}]]> + + + + + + + + + + Data Integrity through Validation Functions + Ensure Data Integrity with Pure Validation Functions + + +- Implement data validation logic as pure functions that take data as input and return validation results (e.g., a list of errors, or an object indicating success/failure). +- These functions should not throw exceptions for validation failures as a primary flow control; instead, they should return information about the validation outcome. +- Multiple validation rules can be composed or chained together. +- Consider using `Optional` or a custom result type to represent validation messages for individual rules. +- Keep validation logic separate from the data structures themselves. +- Make validation rules reusable across different parts of the application. + + + + validateEmailFormat(String email) { + if (Objects.isNull(email) || !EMAIL_PATTERN.matcher(email).matches()) { + return Optional.of("Invalid email format for: " + email); + } + return Optional.empty(); // No error + } + + public static Optional validateAgeRange(int age) { + if (age < 0 || age > 150) { + return Optional.of("Age must be between 0 and 150, but was: " + age); + } + return Optional.empty(); // No error + } + + // Composite validation for a UserRecord + public static List validateUser(UserRecord user) { + return Stream.of( + validateEmailFormat(user.email()), + validateAgeRange(user.age()) + ) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } +} + +class ValidationFunctionExample { + public static void main(String args) { + UserRecord user1 = new UserRecord("u1", "alice@example.com", 30); + List errors1 = UserValidators.validateUser(user1); + if (errors1.isEmpty()) { + System.out.println("User 1 is valid: " + user1); + } else { + System.out.println("User 1 validation errors: " + errors1); + } + + UserRecord user2 = new UserRecord("u2", "bob.com", -5); + List errors2 = UserValidators.validateUser(user2); + if (errors2.isEmpty()) { + System.out.println("User 2 is valid: " + user2); + } else { + System.out.println("User 2 validation errors: " + errors2); + } + } +}]]> + + + + + + + + + + Flexible and Generic Data Access + Design Flexible and Generic Data Access Layers + + +- Define generic interfaces for data access operations (CRUD - Create, Read, Update, Delete). +- Use type parameters (`<T>`) in these interfaces to allow them to work with various data types. +- Implementations of these interfaces can then provide data storage and retrieval using different mechanisms (e.g., in-memory maps, databases, file systems) while the calling code remains decoupled. +- Ensure data storage implementations are thread-safe if they will be accessed concurrently. +- Support common querying needs like filtering or finding by ID. +- Design in a way that allows the underlying storage implementation to be easily swapped or replaced. +- Consider caching strategies at this layer for performance improvement. + + + + { // Added ID type parameter + Optional findById(ID id); + List findAll(); + List findByPredicate(Predicate filter); + void save(ID id, T data); + void deleteById(ID id); + // Potentially add update methods, etc. +} + +// Example record to store +record Product(String id, String name, double price) {} + +// In-memory implementation using generic data structures +class InMemoryDataStore implements DataStore { + private final Map storage = new ConcurrentHashMap<>(); // Thread-safe for concurrent access + + @Override + public Optional findById(ID id) { + return Optional.ofNullable(storage.get(id)); + } + + @Override + public List findAll() { + return List.copyOf(storage.values()); // Return immutable copy + } + + @Override + public List findByPredicate(Predicate filter) { + return storage.values().stream() + .filter(filter) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public void save(ID id, T data) { + storage.put(id, data); + System.out.println("Saved: " + data); + } + + @Override + public void deleteById(ID id) { + storage.remove(id); + System.out.println("Deleted item with id: " + id); + } +} + +class DataAccessExample { + public static void main(String args) { + DataStore productStore = new InMemoryDataStore<>(); + + Product laptop = new Product("P101", "Laptop Pro", 1200.00); + Product mouse = new Product("P102", "Wireless Mouse", 25.00); + + productStore.save(laptop.id(), laptop); + productStore.save(mouse.id(), mouse); + + System.out.println("Find P101: " + productStore.findById("P101")); + System.out.println("All products: " + productStore.findAll()); + System.out.println("Expensive products: " + + productStore.findByPredicate(p -> p.price() > 100.0)); + + productStore.deleteById("P102"); + System.out.println("All products after delete: " + productStore.findAll()); + } +}]]> + + + productTable = new HashMap<>(); + + public Product getProductById(String id) { + return productTable.get(id); // Returns null if not found, no Optional + } + + public void addProduct(Product product) { + // If Product had no ID field, keying would be arbitrary + productTable.put(product.id(), product); + } + // No generic way to handle other types. + // If we need a User DAO, we write a whole new class similar to this. + // Difficult to swap storage implementation. +} + +class BadDataAccessExample { + public static void main(String args) { + // (Using Product record from Good Example) + SpecificProductDao dao = new SpecificProductDao(); + dao.addProduct(new Product("P001", "Old Monitor", 150.0)); + System.out.println("Product P001: " + dao.getProductById("P001")); + // Problems: not generic, not easily testable with mocks if directly instantiated, + // not thread-safe by default, harder to maintain for many data types. + } +}]]> + + + + + + + Explicit and Traceable Data Transformation + Ensure Data Transformations are Explicit and Traceable + + +- Make all data transformation steps visible and easy to follow. +- Use sequences of pure functions for complex transformations, making each step clear. +- Avoid hidden or implicit transformations within objects or complex method calls. +- Consider logging input, output, and intermediate steps of significant transformations, especially during debugging or for auditing. +- Handle potential errors or exceptional cases gracefully within the transformation pipeline. +- Document the overall flow and purpose of complex data transformations. + + + + items) {} + +// Target DTOs +record ProcessedOrderHeader(String orderId, String customerId, double normalizedAmount) {} +record OrderItem(String itemId, int quantity) {} // Assuming items map needs parsing + +class OrderProcessingService { + + // Transformation Step 1: Normalize currency (pure function) + private static Function normalizeCurrency(String targetCurrency, Map rates) { + return rawOrder -> { + if (rawOrder.currency().equals(targetCurrency)) { + return rawOrder; + } + double rate = rates.getOrDefault(rawOrder.currency(), 1.0); // Default to 1.0 if rate not found + double newAmount = rawOrder.amount() * rate; + System.out.println("Trace Normalizing currency for order " + rawOrder.orderId() + ": " + rawOrder.amount() + " " + rawOrder.currency() + " -> " + newAmount + " " + targetCurrency); + return new RawOrder(rawOrder.orderId(), rawOrder.customerId(), newAmount, targetCurrency, rawOrder.items()); + }; + } + + // Transformation Step 2: Extract header (pure function) + private static Function extractHeader() { + return rawOrder -> { + System.out.println("Trace Extracting header for order " + rawOrder.orderId()); + return new ProcessedOrderHeader(rawOrder.orderId(), rawOrder.customerId(), rawOrder.amount()); + }; + } + + // Overall transformation pipeline + public static ProcessedOrderHeader processOrder(RawOrder rawOrder, String targetCurrency, Map exchangeRates) { + System.out.println("Trace Starting processing for order: " + rawOrder.orderId()); + + ProcessedOrderHeader header = normalizeCurrency(targetCurrency, exchangeRates) + .andThen(extractHeader()) // Chaining transformations + .apply(rawOrder); + + System.out.println("Trace Finished processing. Header: " + header); + return header; + // Further steps could parse items, validate, etc. + } +} + +class TransformationTraceExample { + public static void main(String args) { + RawOrder order1 = new RawOrder("ORD123", "CUST001", 100.0, "USD", Map.of("itemA", "2")); + RawOrder order2 = new RawOrder("ORD456", "CUST002", 85.0, "EUR", Map.of("itemB", "1")); + + Map ratesToUSD = Map.of("EUR", 1.08, "USD", 1.0); + + ProcessedOrderHeader processed1 = OrderProcessingService.processOrder(order1, "USD", ratesToUSD); + System.out.println("Processed Order 1: " + processed1); + + ProcessedOrderHeader processed2 = OrderProcessingService.processOrder(order2, "USD", ratesToUSD); + System.out.println("Processed Order 2: " + processed2); + } +}]]> + + + + + + + +
diff --git a/spml/src/main/resources/cursor-rule-generator.xsl b/spml/src/main/resources/cursor-rule-generator.xsl new file mode 100644 index 00000000..fc86554d --- /dev/null +++ b/spml/src/main/resources/cursor-rule-generator.xsl @@ -0,0 +1,387 @@ + + + + + + + + --- +description: + + + + +globs: + + + + +alwaysApply: + +--- +# + + +## System prompt characterization + +Role definition: + + +## Description + + + + + + + + + +## Table of contents + + + + - Rule : + + + + + + + + +## Table of contents + + + + - + + + + + + + + +## Table of contents + + + + - + + + + + + + + + + + + + +## Rule : + + +Title: + +Description: + + + + + + + ** + ** + + + + + * ** + **: + + + + + + + + +**Good example:** + +``` + + + + + + + + + +``` + + + + + +**Bad example:** + +``` + + + + + + + + + +``` + + + + + + + + + + + +## + : + + + + +--- + + + + + + + +## + + : + + + + + + + + + + + + - + + : + + + + + + + + + + +## + + : + + + + + + + + + + + + ### Step : + + + + + + +``` + + + + + + + + + +``` + + + + + + + + + + + +## + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + +## + + + + + + + + + + + + + + + + + + + - + + + + + + + + ### Restrictions + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spml/src/main/resources/spml.xsd b/spml/src/main/resources/spml.xsd new file mode 100644 index 00000000..411454d5 --- /dev/null +++ b/spml/src/main/resources/spml.xsd @@ -0,0 +1,912 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spml/src/test/java/info/jab/xml/CursorRuleGeneratorTest.java b/spml/src/test/java/info/jab/xml/CursorRuleGeneratorTest.java new file mode 100644 index 00000000..aa135fc1 --- /dev/null +++ b/spml/src/test/java/info/jab/xml/CursorRuleGeneratorTest.java @@ -0,0 +1,377 @@ +package info.jab.xml; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("Cursor Rule Generator Tests") +class CursorRuleGeneratorTest { + + private static final Logger logger = LoggerFactory.getLogger(CursorRuleGeneratorTest.class); + + @Nested + @DisplayName("Parameterized Generate Method Tests") + class ParameterizedGenerateMethodTests { + + @Test + @DisplayName("Should throw exception when XML file does not exist") + void should_throwException_when_xmlFileDoesNotExist() { + // Given + CursorRuleGenerator generator = new CursorRuleGenerator(); + + // When & Then - Updated for functional API exception handling + assertThatThrownBy(() -> generator.generate("non-existent.xml", "cursor-rule-generator.xsl")) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Failed to generate cursor rules for") + .hasMessageContaining("non-existent.xml") + .hasMessageContaining("cursor-rule-generator.xsl"); + } + + @Test + @DisplayName("Should throw exception when XSLT file does not exist") + void should_throwException_when_xsltFileDoesNotExist() { + // Given + CursorRuleGenerator generator = new CursorRuleGenerator(); + + // When & Then - Updated for functional API exception handling + assertThatThrownBy(() -> generator.generate("112-java-maven-documentation.xml", "non-existent.xsl")) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Failed to generate cursor rules for") + .hasMessageContaining("112-java-maven-documentation.xml") + .hasMessageContaining("non-existent.xsl"); + } + + /** + * Pure function to load expected content from resources. + * Uses Optional for null safety following functional programming principles. + */ + private String loadExpectedContent(String filename) throws IOException { + return Optional.ofNullable(getClass().getClassLoader().getResourceAsStream(filename)) + .map(inputStream -> { + try (inputStream) { + return new String(inputStream.readAllBytes()).trim(); + } catch (IOException e) { + throw new RuntimeException("Failed to read resource: " + filename, e); + } + }) + .orElseThrow(() -> new IOException("Resource not found: " + filename)); + } + + /** + * Pure function to save generated content to target directory. + * Follows functional programming principles with clear input/output relationship. + */ + private void saveGeneratedContentToTarget(String content, String filename) throws IOException { + Path targetDir = Paths.get("target"); + if (!Files.exists(targetDir)) { + Files.createDirectories(targetDir); + } + Path outputPath = targetDir.resolve(filename); + Files.writeString(outputPath, content); + logger.info("Generated content saved to: {}", outputPath.toAbsolutePath()); + } + } + + @Nested + @DisplayName("Unified XSLT Generator Tests") + class UnifiedXsltGeneratorTests { + + @Test + @DisplayName("Should generate exact content matching original expected Maven best practices document using unified XSLT") + void should_generateExactContentMatchingOriginalExpected_when_transformingMavenBestPracticesWithUnifiedXslt() throws IOException { + // Given + CursorRuleGenerator generator = new CursorRuleGenerator(); + String expectedContent = loadExpectedContent("110-java-maven-best-practices.mdc"); + + // When + String actualResult = generator.generate("110-java-maven-best-practices.xml", "cursor-rule-generator.xsl"); + + // Then - Unified XSLT should produce identical output to specialized XSLT + assertThat(actualResult) + .isNotNull() + .isNotEmpty() + .isEqualTo(expectedContent); + } + + @Test + @DisplayName("Should generate exact content matching original expected Maven documentation document using unified XSLT") + void should_generateExactContentMatchingOriginalExpected_when_transformingMavenDocumentationWithUnifiedXslt() throws IOException { + // Given + CursorRuleGenerator generator = new CursorRuleGenerator(); + String expectedContent = loadExpectedContent("112-java-maven-documentation.mdc"); + + // When + String actualResult = generator.generate("112-java-maven-documentation.xml", "cursor-rule-generator.xsl"); + + // Then - Unified XSLT should produce identical output to specialized XSLT + assertThat(actualResult) + .isNotNull() + .isNotEmpty() + .isEqualTo(expectedContent); + } + + @Test + @DisplayName("Should generate exact content matching original expected Java checklist guide document using unified XSLT") + void should_generateExactContentMatchingOriginalExpected_when_transformingJavaChecklistGuideWithUnifiedXslt() throws IOException { + // Given + CursorRuleGenerator generator = new CursorRuleGenerator(); + String expectedContent = loadExpectedContent("100-java-checklist-guide.mdc"); + + // When + String actualResult = generator.generate("100-java-checklist-guide.xml", "cursor-rule-generator.xsl"); + + // Then - Unified XSLT should produce identical output to expected + assertThat(actualResult) + .isNotNull() + .isNotEmpty() + .isEqualTo(expectedContent); + } + + @Test + @DisplayName("Should generate exact content matching original expected Java Object-Oriented Design document using unified XSLT") + void should_generateExactContentMatchingOriginalExpected_when_transformingJavaObjectOrientedDesignWithUnifiedXslt() throws IOException { + // Given + CursorRuleGenerator generator = new CursorRuleGenerator(); + String expectedContent = loadExpectedContent("121-java-object-oriented-design.mdc"); + + // When + String actualResult = generator.generate("121-java-object-oriented-design.xml", "cursor-rule-generator.xsl"); + + // Then - Unified XSLT should produce identical output to expected + assertThat(actualResult) + .isNotNull() + .isNotEmpty() + .isEqualTo(expectedContent); + } + + @Test + @DisplayName("Should generate exact content matching original expected Java Type Design document using unified XSLT") + void should_generateExactContentMatchingOriginalExpected_when_transformingJavaTypeDesignWithUnifiedXslt() throws IOException { + // Given + CursorRuleGenerator generator = new CursorRuleGenerator(); + String expectedContent = loadExpectedContent("122-java-type-design.mdc"); + + // When + String actualResult = generator.generate("122-java-type-design.xml", "cursor-rule-generator.xsl"); + + // Then - Unified XSLT should produce identical output to expected + assertThat(actualResult) + .isNotNull() + .isNotEmpty() + .isEqualTo(expectedContent); + } + + @Test + @DisplayName("Should generate exact content matching original expected Java General Guidelines document using unified XSLT") + void should_generateExactContentMatchingOriginalExpected_when_transformingJavaGeneralGuidelinesWithUnifiedXslt() throws IOException { + // Given + CursorRuleGenerator generator = new CursorRuleGenerator(); + String expectedContent = loadExpectedContent("123-java-general-guidelines.mdc"); + + // When + String actualResult = generator.generate("123-java-general-guidelines.xml", "cursor-rule-generator.xsl"); + + // Then - Unified XSLT should produce identical output to expected + assertThat(actualResult) + .isNotNull() + .isNotEmpty() + .isEqualTo(expectedContent); + } + + @Test + @DisplayName("Should generate exact content matching original expected Java Secure Coding document using unified XSLT") + void should_generateExactContentMatchingOriginalExpected_when_transformingJavaSecureCodingWithUnifiedXslt() throws IOException { + // Given + CursorRuleGenerator generator = new CursorRuleGenerator(); + String expectedContent = loadExpectedContent("124-java-secure-coding.mdc"); + + // When + String actualResult = generator.generate("124-java-secure-coding.xml", "cursor-rule-generator.xsl"); + + // Then - Unified XSLT should produce identical output to expected + assertThat(actualResult) + .isNotNull() + .isNotEmpty() + .isEqualTo(expectedContent); + } + + @Test + @DisplayName("Should generate exact content matching original expected Java Concurrency document using unified XSLT") + void should_generateExactContentMatchingOriginalExpected_when_transformingJavaConcurrencyWithUnifiedXslt() throws IOException { + // Given + CursorRuleGenerator generator = new CursorRuleGenerator(); + String expectedContent = loadExpectedContent("125-java-concurrency.mdc"); + + // When + String actualResult = generator.generate("125-java-concurrency.xml", "cursor-rule-generator.xsl"); + + // Then - Unified XSLT should produce identical output to expected + assertThat(actualResult) + .isNotNull() + .isNotEmpty() + .isEqualTo(expectedContent); + } + + @Test + @DisplayName("Should generate exact content matching original expected Java Logging document using unified XSLT") + void should_generateExactContentMatchingOriginalExpected_when_transformingJavaLoggingWithUnifiedXslt() throws IOException { + // Given + CursorRuleGenerator generator = new CursorRuleGenerator(); + String expectedContent = loadExpectedContent("126-java-logging.mdc"); + + // When + String actualResult = generator.generate("126-java-logging.xml", "cursor-rule-generator.xsl"); + + // Then - Unified XSLT should produce identical output to expected + assertThat(actualResult) + .isNotNull() + .isNotEmpty() + .isEqualTo(expectedContent); + } + + @Test + @DisplayName("Should generate exact content matching original expected Java Unit Testing document using unified XSLT") + void should_generateExactContentMatchingOriginalExpected_when_transformingJavaUnitTestingWithUnifiedXslt() throws IOException { + // Given + CursorRuleGenerator generator = new CursorRuleGenerator(); + String expectedContent = loadExpectedContent("131-java-unit-testing.mdc"); + + // When + String actualResult = generator.generate("131-java-unit-testing.xml", "cursor-rule-generator.xsl"); + + // Then - Unified XSLT should produce identical output to expected + assertThat(actualResult) + .isNotNull() + .isNotEmpty() + .isEqualTo(expectedContent); + } + + @Test + @DisplayName("Should generate exact content matching original expected Java Refactoring with Modern Features document using unified XSLT") + void should_generateExactContentMatchingOriginalExpected_when_transformingJavaRefactoringWithModernFeaturesWithUnifiedXslt() throws IOException { + // Given + CursorRuleGenerator generator = new CursorRuleGenerator(); + String expectedContent = loadExpectedContent("141-java-refactoring-with-modern-features.mdc"); + + // When + String actualResult = generator.generate("141-java-refactoring-with-modern-features.xml", "cursor-rule-generator.xsl"); + + // Then - Unified XSLT should produce identical output to expected + assertThat(actualResult) + .isNotNull() + .isNotEmpty() + .isEqualTo(expectedContent); + } + + @Test + @DisplayName("Should generate exact content matching original expected Java Data-Oriented Programming document using unified XSLT") + void should_generateExactContentMatchingOriginalExpected_when_transformingJavaDataOrientedProgrammingWithUnifiedXslt() throws IOException { + // Given + CursorRuleGenerator generator = new CursorRuleGenerator(); + String expectedContent = loadExpectedContent("143-java-data-oriented-programming.mdc"); + + // When + String actualResult = generator.generate("143-java-data-oriented-programming.xml", "cursor-rule-generator.xsl"); + + // Then - Unified XSLT should produce identical output to expected + assertThat(actualResult) + .isNotNull() + .isNotEmpty() + .isEqualTo(expectedContent); + } + + @Test + @DisplayName("Should generate exact content matching original expected Java Functional Programming document using unified XSLT") + void should_generateExactContentMatchingOriginalExpected_when_transformingJavaFunctionalProgrammingWithUnifiedXslt() throws IOException { + // Given + CursorRuleGenerator generator = new CursorRuleGenerator(); + String expectedContent = loadExpectedContent("142-java-functional-programming.mdc"); + + // When + String actualResult = generator.generate("142-java-functional-programming.xml", "cursor-rule-generator.xsl"); + + // Then - Unified XSLT should produce identical output to expected + assertThat(actualResult) + .isNotNull() + .isNotEmpty() + .isEqualTo(expectedContent); + } + + /** + * Pure function to load expected content from resources. + * Uses Optional for null safety following functional programming principles. + */ + private String loadExpectedContent(String filename) throws IOException { + return Optional.ofNullable(getClass().getClassLoader().getResourceAsStream(filename)) + .map(inputStream -> { + try (inputStream) { + return new String(inputStream.readAllBytes()).trim(); + } catch (IOException e) { + throw new RuntimeException("Failed to read resource: " + filename, e); + } + }) + .orElseThrow(() -> new IOException("Resource not found: " + filename)); + } + } + + @Test + @DisplayName("Should produce consistent content structure regardless of XML content type") + void should_produceConsistentStructure_when_processingDifferentXmlTypes() throws IOException { + // Given + CursorRuleGenerator generator = new CursorRuleGenerator(); + + // When + String checklistGuideResult = generator.generate("100-java-checklist-guide.xml", "cursor-rule-generator.xsl"); + String bestPracticesResult = generator.generate("110-java-maven-best-practices.xml", "cursor-rule-generator.xsl"); + String documentationResult = generator.generate("112-java-maven-documentation.xml", "cursor-rule-generator.xsl"); + String objectOrientedDesignResult = generator.generate("121-java-object-oriented-design.xml", "cursor-rule-generator.xsl"); + String typeDesignResult = generator.generate("122-java-type-design.xml", "cursor-rule-generator.xsl"); + String generalGuidelinesResult = generator.generate("123-java-general-guidelines.xml", "cursor-rule-generator.xsl"); + String secureCodingResult = generator.generate("124-java-secure-coding.xml", "cursor-rule-generator.xsl"); + String concurrencyResult = generator.generate("125-java-concurrency.xml", "cursor-rule-generator.xsl"); + String loggingResult = generator.generate("126-java-logging.xml", "cursor-rule-generator.xsl"); + String unitTestingResult = generator.generate("131-java-unit-testing.xml", "cursor-rule-generator.xsl"); + String refactoringWithModernFeaturesResult = generator.generate("141-java-refactoring-with-modern-features.xml", "cursor-rule-generator.xsl"); + String functionalProgrammingResult = generator.generate("142-java-functional-programming.xml", "cursor-rule-generator.xsl"); + String dataOrientedProgrammingResult = generator.generate("143-java-data-oriented-programming.xml", "cursor-rule-generator.xsl"); + + // Save all for comparison + saveGeneratedContentToTarget(checklistGuideResult, "100-java-checklist-guide.mdc"); + //saveGeneratedContentToTarget(bestPracticesResult, "unified-best-practices.mdc"); + //saveGeneratedContentToTarget(documentationResult, "unified-documentation.mdc"); + //saveGeneratedContentToTarget(objectOrientedDesignResult, "unified-object-oriented-design.mdc"); + //saveGeneratedContentToTarget(typeDesignResult, "unified-type-design.mdc"); + //saveGeneratedContentToTarget(generalGuidelinesResult, "unified-general-guidelines.mdc"); + //saveGeneratedContentToTarget(secureCodingResult, "unified-secure-coding.mdc"); + //saveGeneratedContentToTarget(concurrencyResult, "unified-concurrency.mdc"); + //saveGeneratedContentToTarget(loggingResult, "unified-logging.mdc"); + //saveGeneratedContentToTarget(unitTestingResult, "unified-unit-testing.mdc"); + //saveGeneratedContentToTarget(refactoringWithModernFeaturesResult, "unified-refactoring-with-modern-features.mdc"); + //saveGeneratedContentToTarget(dataOrientedProgrammingResult, "unified-data-oriented-programming.mdc"); + //saveGeneratedContentToTarget(functionalProgrammingResult, "unified-functional-programming.mdc"); + } + + /** + * Pure function to save generated content to target directory. + * Follows functional programming principles with clear input/output relationship. + */ + private void saveGeneratedContentToTarget(String content, String filename) throws IOException { + Path targetDir = Paths.get("target"); + if (!Files.exists(targetDir)) { + Files.createDirectories(targetDir); + } + Path outputPath = targetDir.resolve(filename); + Files.writeString(outputPath, content); + logger.info("Generated content saved to: {}", outputPath.toAbsolutePath()); + } +} diff --git a/spml/src/test/resources/100-java-checklist-guide.mdc b/spml/src/test/resources/100-java-checklist-guide.mdc new file mode 100644 index 00000000..fd5ccd77 --- /dev/null +++ b/spml/src/test/resources/100-java-checklist-guide.mdc @@ -0,0 +1,36 @@ +--- +description: +globs: +alwaysApply: false +--- +# Create a Checklist with all Java steps to use with cursor rules for Java + +## System prompt characterization + +Role definition: You are a Senior software engineer with extensive experience in Java programming language and technical documentation + +## Description + +Your task is to create a comprehensive step-by-step guide that follows the exact format and structure defined in the embedded template below. + +## Instructions for AI + +Create a markdown file named `JAVA-DEVELOPMENT-GUIDE.md` with the following exact structure: [java-checklist-template.md](mdc:.cursor/rules/templates/java-checklist-template.md) + +### Restrictions + +**MANDATORY REQUIREMENT**: Follow the embedded template EXACTLY - do not add, remove, or modify any steps, sections, or cursor rules that are not explicitly shown in the template. ### What NOT to Include: + +- **DO NOT** create additional steps beyond what's shown in the template +- **DO NOT** add cursor rules that are not explicitly listed in the embedded template +- **DO NOT** expand or elaborate on sections beyond what the template shows +- **ONLY** use cursor rules that appear in the embedded template +- **ONLY** use the exact wording and structure from the template +- If a cursor rule exists in the workspace but is not in the template, **DO NOT** include it + + +## Output Requirements + +- Generate the complete markdown file following the embedded template exactly +- Use proper markdown formatting with headers, code blocks, tables, and checklists +- **VERIFY**: Final output contains ONLY what appears in the embedded template diff --git a/spml/src/test/resources/110-java-maven-best-practices.mdc b/spml/src/test/resources/110-java-maven-best-practices.mdc new file mode 100644 index 00000000..2dae4877 --- /dev/null +++ b/spml/src/test/resources/110-java-maven-best-practices.mdc @@ -0,0 +1,571 @@ +--- +description: Maven Best Practices +globs: pom.xml +alwaysApply: false +--- +# Maven Best Practices + +## System prompt characterization + +Role definition: You are a Senior software engineer with extensive experience in Java software development + +## Description + +Effective Maven usage involves robust dependency management via `` and BOMs, adherence to the standard directory layout, and centralized plugin management. Build profiles should be used for environment-specific configurations. POMs must be kept readable and maintainable with logical structure and properties for versions. Custom repositories should be declared explicitly and their use minimized, preferably managed via a central repository manager. + +## Table of contents + +- Rule 1: Effective Dependency Management +- Rule 2: Standard Directory Layout +- Rule 3: Plugin Management and Configuration +- Rule 4: Use Build Profiles for Environment-Specific Configurations +- Rule 5: Keep POMs Readable and Maintainable +- Rule 6: Manage Repositories Explicitly +- Rule 7: Centralize Version Management with Properties + +## Rule 1: Effective Dependency Management + +Title: Manage Dependencies Effectively using `dependencyManagement` and BOMs +Description: Use the `` section in parent POMs or import Bill of Materials (BOMs) to centralize and control dependency versions. This helps avoid version conflicts and ensures consistency across multi-module projects. Avoid hardcoding versions directly in `` when managed elsewhere. + +**Good example:** + +```xml + + + 4.0.0 + com.example + my-parent + 1.0.0 + pom + + + 5.3.23 + 5.9.0 + + + + + + org.springframework + spring-context + ${spring.version} + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + + org.springframework.boot + spring-boot-dependencies + 2.7.5 + pom + import + + + + + + + + 4.0.0 + + com.example + my-parent + 1.0.0 + + my-module + + + + org.springframework + spring-context + + + + org.junit.jupiter + junit-jupiter-api + + + + +``` + +**Bad example:** + +```xml + + + 4.0.0 + com.example + my-other-module + 1.0.0 + + + + org.springframework + spring-context + 5.3.20 + + + org.junit.jupiter + junit-jupiter-api + 5.8.1 + test + + + +``` + +## Rule 2: Standard Directory Layout + +Title: Adhere to the Standard Directory Layout +Description: Follow Maven's convention for directory structure (`src/main/java`, `src/main/resources`, `src/test/java`, `src/test/resources`, etc.). This makes projects easier to understand and build, as Maven relies on these defaults. + +**Good example:** + +``` +my-app/ +├── pom.xml +└── src/ + ├── main/ + │ ├── java/ + │ │ └── com/example/myapp/App.java + │ └── resources/ + │ └── application.properties + └── test/ + ├── java/ + │ └── com/example/myapp/AppTest.java + └── resources/ + └── test-data.xml +``` + +**Bad example:** + +``` +my-app/ +├── pom.xml +├── sources/ +│ └── com/example/myapp/App.java +├── res/ +│ └── config.properties +└── tests/ + └── com/example/myapp/AppTest.java + +``` + +## Rule 3: Plugin Management and Configuration + +Title: Manage Plugin Versions and Configurations Centrally +Description: Use `` in a parent POM to define plugin versions and common configurations. Child POMs can then use the plugins without specifying versions, ensuring consistency. Override configurations in child POMs only when necessary. + +**Good example:** + +```xml + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.1 + + 17 + 17 + + + + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + +``` + +**Bad example:** + +```xml + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 11 + 11 + + + + + +``` + +## Rule 4: Use Build Profiles for Environment-Specific Configurations + +Title: Employ Build Profiles for Environment-Specific Settings +Description: Use Maven profiles to customize build settings for different environments (e.g., dev, test, prod) or other conditional scenarios. This can include different dependencies, plugin configurations, or properties. Activate profiles via command line, OS, JDK, or file presence. + +**Good example:** + +```xml + + + + + dev + + true + + + jdbc:h2:mem:devdb + + + + prod + + jdbc:postgresql://prodserver/mydb + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + package + run + + + + Simulating minification for prod + + + + + + + + + + + + +``` + +**Bad example:** + +```xml + + + + + + jdbc:postgresql://prodserver/mydb + + +``` + +## Rule 5: Keep POMs Readable and Maintainable + +Title: Structure POMs Logically for Readability +Description: Organize your `pom.xml` sections in a consistent order (e.g., project coordinates, parent, properties, dependencyManagement, dependencies, build, profiles, repositories). Use properties for recurring versions or values. Add comments for complex configurations. + +**Good example:** + +```xml + + 4.0.0 + + + com.example + my-app + 1.0.0-SNAPSHOT + jar + My Application + A sample application. + + + + + + + 17 + UTF-8 + 2.5.1 + + + + + + + + + + + org.some.library + some-library-core + ${some.library.version} + + + + + + + + + + + + + + + +``` + +**Bad example:** + +```xml + + + + + org.some.library + some-library-core + 2.5.1 + + + 4.0.0 + + + + com.example + + UTF-8 + + my-app + 1.0.0-SNAPSHOT + +``` + +## Rule 6: Manage Repositories Explicitly + +Title: Declare Custom Repositories Explicitly and Minimize Their Use +Description: Prefer dependencies from Maven Central. If custom repositories are necessary, declare them in the `` section and `` for plugins. It's often better to manage these in a company-wide Nexus/Artifactory instance configured in `settings.xml` rather than per-project POMs. Avoid relying on transitive repositories. + +**Good example:** + +```xml + + + + + my-internal-repo + https://nexus.example.com/repository/maven-releases/ + + + + + my-internal-plugins + https://nexus.example.com/repository/maven-plugins/ + + + + +``` + +**Bad example:** + +```xml + + + + + + com.internal.stuff + internal-lib + 1.0 + + + + +``` + +## Rule 7: Centralize Version Management with Properties + +Title: Use Properties to Manage Dependency and Plugin Versions +Description: Define all dependency and plugin versions in the `` section rather than hardcoding them throughout the POM. This centralizes version management, makes updates easier, reduces duplication, and helps maintain consistency across related dependencies. Use consistent property naming conventions: `maven-plugin-[name].version` for Maven plugins, simple names like `[library].version` for dependencies, and descriptive names for quality thresholds like `coverage.level`. + +**Good example:** + +```xml + + 4.0.0 + com.example + my-app + 1.0.0 + + + + 17 + 3.9.10 + UTF-8 + UTF-8 + + + 2.15.3 + 5.10.1 + 5.7.0 + 1.4.11 + + + 3.14.0 + 3.5.3 + 3.5.3 + 3.5.0 + + + 0.8.13 + + + 80 + 70 + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-plugin-compiler.version} + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-plugin-surefire.version} + + + org.jacoco + jacoco-maven-plugin + ${maven-plugin-jacoco.version} + + + + +``` + +**Bad example:** + +```xml + + + 4.0.0 + com.example + my-app + 1.0.0 + + + 17 + UTF-8 + + + + + com.fasterxml.jackson.core + jackson-databind + 2.15.3 + + + com.fasterxml.jackson.core + jackson-core + 2.15.2 + + + org.junit.jupiter + junit-jupiter-api + 5.10.1 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.9.3 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.2 + + + org.jacoco + jacoco-maven-plugin + 0.8.10 + + + + +``` diff --git a/spml/src/test/resources/112-java-maven-documentation.mdc b/spml/src/test/resources/112-java-maven-documentation.mdc new file mode 100644 index 00000000..d7a14f0e --- /dev/null +++ b/spml/src/test/resources/112-java-maven-documentation.mdc @@ -0,0 +1,56 @@ +--- +description: Create README-DEV.md with information about how to use the Maven project +globs: pom.xml +alwaysApply: false +--- +# Create README-DEV.md with information about how to use the Maven project + +## System prompt characterization + +Role definition: You are a Senior software engineer with extensive experience in Java software development + +## Description + +When creating a README-DEV.md file for a Maven project, include ONLY the following sections with the specified Maven goals. Do NOT add any additional sections, explanations, or content beyond what is explicitly listed below. + +## STRICT Structure for README-DEV.md (Template): + +**IMPORTANT: Include ONLY the content specified below.** + +--- + +# Essential Maven Goals: + +```bash +# Analyze dependencies +./mvnw dependency:tree +./mvnw dependency:analyze +./mvnw dependency:resolve + +./mvnw clean validate -U +./mvnw buildplan:list-plugin +./mvnw buildplan:list-phase +./mvnw help:all-profiles +./mvnw help:active-profiles +./mvnw license:third-party-report + +# Clean the project +./mvnw clean + +# Clean and package in one command +./mvnw clean package + +# Run integration tests +./mvnw verify + +# Check for dependency updates +./mvnw versions:display-property-updates +./mvnw versions:display-dependency-updates +./mvnw versions:display-plugin-updates + +# Generate project reports +./mvnw site +jwebserver -p 8005 -d "$(pwd)/target/site/" +``` + +**END OF TEMPLATE - DO NOT ADD ANYTHING BEYOND THIS POINT** \ No newline at end of file diff --git a/spml/src/test/resources/121-java-object-oriented-design.mdc b/spml/src/test/resources/121-java-object-oriented-design.mdc new file mode 100644 index 00000000..0e4a5002 --- /dev/null +++ b/spml/src/test/resources/121-java-object-oriented-design.mdc @@ -0,0 +1,294 @@ +--- +description: Java Object-Oriented Design Guidelines +globs: *.java +alwaysApply: false +--- +# Java Object-Oriented Design Guidelines + +## System prompt characterization + +Role definition: You are a Senior software engineer with extensive experience in Java software development + +## Description + +This document provides comprehensive guidelines for robust Java object-oriented design and refactoring. It emphasizes core principles like SOLID, DRY, and YAGNI, best practices for class and interface design including favoring composition over inheritance and designing for immutability. The rules also cover mastering encapsulation, inheritance, and polymorphism, and finally, identifying and refactoring common object-oriented design code smells such as God Classes, Feature Envy, and Data Clumps to promote maintainable, flexible, and understandable code. + +## Table of contents + +- Rule 1: Adhere to Core Design Principles (SOLID, DRY, YAGNI) +- Rule 2: Follow Best Practices for Class and Interface Design +- Rule 3: Master Encapsulation, Inheritance, and Polymorphism +- Rule 4: Identify and Refactor Object-Oriented Design Code Smells + +## Rule 1: Adhere to Core Design Principles (SOLID, DRY, YAGNI) + +Title: Apply Fundamental Software Design Principles +Description: Core principles like SOLID, DRY, and YAGNI are foundational to good object-oriented design, leading to more robust, maintainable, and understandable systems. These principles should guide all design decisions and help create systems that are flexible and resistant to change. + +**Good example:** + +```java +// Good: Separate responsibilities following SRP +class UserData { + private String name; + private String email; + + public UserData(String name, String email) { + this.name = name; + this.email = email; + } + + public String getName() { return name; } + public String getEmail() { return email; } +} + +class UserPersistence { + public void saveUser(UserData user) { + System.out.println("Saving user " + user.getName() + " to database."); + // Database saving logic + } +} + +class UserEmailer { + public void sendWelcomeEmail(UserData user) { + System.out.println("Sending welcome email to " + user.getEmail()); + // Email sending logic + } +} +``` + +**Bad example:** + +```java +// Bad: User class with multiple responsibilities +class User { + private String name; + private String email; + + public User(String name, String email) { this.name = name; this.email = email; } + + public String getName() { return name; } + public String getEmail() { return email; } + + public void saveToDatabase() { + System.out.println("Saving user " + name + " to database."); + // Database logic mixed in + } + + public void sendWelcomeEmail() { + System.out.println("Sending welcome email to " + email); + // Email logic mixed in + } + // If email sending changes, or DB logic changes, this class needs to change. +} +``` + +## Rule 2: Follow Best Practices for Class and Interface Design + +Title: Design Well-Structured and Maintainable Classes and Interfaces +Description: Good class and interface design is crucial for building flexible and understandable OOD systems. Favor composition over inheritance, program to interfaces rather than implementations, keep classes small and focused, and design for immutability where appropriate. Use clear, descriptive naming conventions. + +**Good example:** + +```java +// Interface (Abstraction) +interface Engine { + void start(); + void stop(); +} + +// Concrete Implementations +class PetrolEngine implements Engine { + @Override public void start() { System.out.println("Petrol engine started."); } + @Override public void stop() { System.out.println("Petrol engine stopped."); } +} + +class ElectricEngine implements Engine { + @Override public void start() { System.out.println("Electric engine silently started."); } + @Override public void stop() { System.out.println("Electric engine silently stopped."); } +} + +// Class using Composition and Programming to an Interface +class Car { + private final Engine engine; // Depends on Engine interface (abstraction) + private final String modelName; + + // Engine is injected (composition) + public Car(String modelName, Engine engine) { + this.modelName = modelName; + this.engine = engine; + } + + public void startCar() { + System.out.print(modelName + ": "); + engine.start(); + } + + public void stopCar() { + System.out.print(modelName + ": "); + engine.stop(); + } + + public String getModelName() { return modelName; } +} +``` + +**Bad example:** + +```java +// Bad: Tight coupling, not programming to an interface +class BadCar { + private final BadPetrolEngine engine; // Direct dependency on concrete BadPetrolEngine + + public BadCar() { + this.engine = new BadPetrolEngine(); // Instantiates concrete class + } + + public void start() { engine.startPetrol(); } + // If we want an electric car, this class needs significant changes or a new similar class. +} + +class BadPetrolEngine { + public void startPetrol() { System.out.println("Bad petrol engine starts."); } +} +``` + +## Rule 3: Master Encapsulation, Inheritance, and Polymorphism + +Title: Effectively Utilize Core Object-Oriented Concepts +Description: Encapsulation, Inheritance, and Polymorphism are the three pillars of object-oriented programming. Proper encapsulation protects internal state and exposes behavior through well-defined interfaces. Inheritance should model true "is-a" relationships following the Liskov Substitution Principle. Polymorphism allows objects of different types to be treated uniformly. + +**Good example:** + +```java +// Good: Proper encapsulation with validation +class BankAccount { + private double balance; // Encapsulated - cannot be directly accessed + private final String accountNumber; + + public BankAccount(String accountNumber, double initialBalance) { + if (initialBalance < 0) { + throw new IllegalArgumentException("Initial balance cannot be negative"); + } + this.accountNumber = accountNumber; + this.balance = initialBalance; + } + + public void deposit(double amount) { + if (amount <= 0) { + throw new IllegalArgumentException("Deposit amount must be positive"); + } + balance += amount; + } + + public boolean withdraw(double amount) { + if (amount <= 0) { + throw new IllegalArgumentException("Withdrawal amount must be positive"); + } + if (amount <= balance) { + balance -= amount; + return true; + } + return false; + } + + public double getBalance() { return balance; } // Controlled access + public String getAccountNumber() { return accountNumber; } +} +``` + +**Bad example:** + +```java +// Bad: Poor encapsulation - direct field access +class BadBankAccount { + public double balance; // Public field - no validation or control + public String accountNumber; // Can be modified directly + + public BadBankAccount(String accountNumber, double initialBalance) { + this.accountNumber = accountNumber; + this.balance = initialBalance; // No validation + } + + // No methods to control access or validate operations +} + +// Usage problems: +// BadBankAccount account = new BadBankAccount("123", 100); +// account.balance = -500; // Negative balance allowed! +// account.accountNumber = null; // Can break account number +``` + +## Rule 4: Identify and Refactor Object-Oriented Design Code Smells + +Title: Recognize and Fix Common Design Anti-Patterns +Description: Develop the ability to identify common object-oriented design "code smells" such as God Class, Feature Envy, Data Clumps, and Refused Bequest. Recognizing and refactoring these smells is crucial for improving the long-term health, maintainability, and clarity of the codebase. + +**Good example:** + +```java +// Good: Refactored from God Class - separate responsibilities +class Order { + private final String orderId; + private final List items; + private final Customer customer; + + public Order(String orderId, Customer customer) { + this.orderId = orderId; + this.customer = customer; + this.items = new ArrayList<>(); + } + + public void addItem(OrderItem item) { items.add(item); } + public List getItems() { return Collections.unmodifiableList(items); } + public Customer getCustomer() { return customer; } + public String getOrderId() { return orderId; } +} + +class OrderCalculator { + public BigDecimal calculateTotal(Order order) { + return order.getItems().stream() + .map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } +} + +class OrderValidator { + public boolean isValid(Order order) { + return !order.getItems().isEmpty() && + order.getCustomer() != null && + order.getCustomer().hasValidPaymentMethod(); + } +} +``` + +**Bad example:** + +```java +// Bad: God Class - doing too many things +class OrderManager { + private String orderId; + private List items; + private Customer customer; + private PaymentProcessor paymentProcessor; + private InventoryService inventoryService; + private EmailService emailService; + private TaxCalculator taxCalculator; + + // Dozens of methods doing various unrelated things: + public void addItem(OrderItem item) { /* ... */ } + public void removeItem(String itemId) { /* ... */ } + public BigDecimal calculateSubtotal() { /* ... */ } + public BigDecimal calculateTax() { /* ... */ } + public BigDecimal calculateShipping() { /* ... */ } + public boolean validateInventory() { /* ... */ } + public void processPayment() { /* ... */ } + public void sendConfirmationEmail() { /* ... */ } + public void updateInventory() { /* ... */ } + public void generateInvoice() { /* ... */ } + public void trackShipment() { /* ... */ } + // ... many more methods + + // This class has too many responsibilities and reasons to change +} +``` diff --git a/spml/src/test/resources/122-java-type-design.mdc b/spml/src/test/resources/122-java-type-design.mdc new file mode 100644 index 00000000..8b8cdf81 --- /dev/null +++ b/spml/src/test/resources/122-java-type-design.mdc @@ -0,0 +1,482 @@ +--- +description: Type Design Thinking in Java +globs: *.java +alwaysApply: false +--- +# Type Design Thinking in Java + +## System prompt characterization + +Role definition: You are a Senior software engineer with extensive experience in Java software development + +## Description + +Type design thinking in Java applies typography principles to code structure and organization. Just as typography creates readable, accessible text, thoughtful type design in Java produces maintainable, comprehensible code. This document focuses on establishing clear type hierarchies, using consistent naming conventions, leveraging generics effectively, and creating type-safe wrappers that communicate intent clearly. + +## Table of contents + +- Rule 1: Establish a Clear Type Hierarchy +- Rule 2: Use Consistent Naming Conventions +- Rule 3: Create Type-Safe Wrappers +- Rule 4: Leverage Generic Type Parameters +- Rule 5: Use BigDecimal for Precision-Sensitive Calculations + +## Rule 1: Establish a Clear Type Hierarchy + +Title: Organize Classes and Interfaces into Logical Structure +Description: This rule focuses on organizing classes and interfaces into a logical structure using inheritance and composition. A clear hierarchy makes the relationships between types explicit, improving code navigation and understanding. It often involves using nested static classes for closely related types. + +**Good example:** + +```java +// GOOD: Clear type hierarchy with descriptive names +public class OrderManagement { + public static class Order { + private List items; + private Customer customer; + private OrderStatus status; + + public Order(Customer customer) { + this.customer = customer; + this.items = new ArrayList<>(); + this.status = OrderStatus.PENDING; + } + + public void addItem(OrderItem item) { items.add(item); } + public List getItems() { return Collections.unmodifiableList(items); } + public Customer getCustomer() { return customer; } + public OrderStatus getStatus() { return status; } + } + + public static class OrderItem { + private Product product; + private int quantity; + private BigDecimal unitPrice; + + public OrderItem(Product product, int quantity, BigDecimal unitPrice) { + this.product = product; + this.quantity = quantity; + this.unitPrice = unitPrice; + } + + public Product getProduct() { return product; } + public int getQuantity() { return quantity; } + public BigDecimal getUnitPrice() { return unitPrice; } + public BigDecimal getTotalPrice() { return unitPrice.multiply(BigDecimal.valueOf(quantity)); } + } + + public enum OrderStatus { + PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED + } +} +``` + +**Bad example:** + +```java +// AVOID: Flat structure with ambiguous names +public class Order { + private List items; // What kind of item? + private User user; // Is this a customer, admin, or something else? + private int status; // What do the numbers mean? + // ... +} + +public class Item { // Too generic - what kind of item? + private Thing thing; // What is a "thing"? + private int count; + // ... +} + +public class User { // Too generic - could be any type of user + private String data; // What kind of data? + // ... +} +``` + +## Rule 2: Use Consistent Naming Conventions + +Title: Apply Uniform Patterns for Naming (Your Type's "Font Family") +Description: This rule emphasizes using uniform patterns for naming classes, interfaces, methods, and variables. Consistency in naming acts like a consistent font family in typography, making the code easier to read, predict, and maintain across the entire project. + +**Good example:** + +```java +// GOOD: Consistent naming patterns +interface PaymentProcessor { + PaymentResult process(Payment payment); +} + +interface ShippingCalculator { + BigDecimal calculate(ShippingRequest request); +} + +interface TaxProvider { + Tax calculateTax(TaxableItem item, Address address); +} + +// Implementation classes follow consistent naming +class StripePaymentProcessor implements PaymentProcessor { + @Override + public PaymentResult process(Payment payment) { + // Stripe-specific implementation + return new PaymentResult(true, "Payment processed successfully"); + } +} + +class StandardShippingCalculator implements ShippingCalculator { + @Override + public BigDecimal calculate(ShippingRequest request) { + // Standard shipping calculation logic + return request.getWeight().multiply(new BigDecimal("0.50")); + } +} +``` + +**Bad example:** + +```java +// AVOID: Inconsistent naming patterns +interface PaymentProcessor { + void handlePayment(Payment p); // Different method naming style +} + +interface ShipCalc { // Inconsistent interface naming + BigDecimal getShippingCost(Order o); // Different parameter naming +} + +interface TaxSystem { // Different naming convention + Tax lookupTaxRate(Address addr); // Abbreviated parameter name +} + +// Implementation classes also inconsistent +class PaymentHandler implements PaymentProcessor { // Handler vs Processor + @Override + public void handlePayment(Payment p) { + // Implementation + } +} + +class ShippingCostCalculatorImpl implements ShipCalc { // Too verbose + @Override + public BigDecimal getShippingCost(Order o) { + // Implementation + } +} +``` + +## Rule 3: Create Type-Safe Wrappers + +Title: Use Types as Communication Tools +Description: This rule encourages wrapping primitive types or general-purpose types (like String) in domain-specific types. These wrapper types enhance type safety by enforcing invariants at compile-time and clearly communicate the intended meaning and constraints of data. + +**Good example:** + +```java +// GOOD: Type-safe wrappers communicate intent +public class EmailAddress { + private final String value; + + public EmailAddress(String email) { + if (!isValid(email)) { + throw new IllegalArgumentException("Invalid email format: " + email); + } + this.value = email; + } + + public String getValue() { + return value; + } + + private boolean isValid(String email) { + return email != null && + email.matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$") && + email.length() <= 254; // RFC 5321 limit + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + EmailAddress that = (EmailAddress) obj; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return value; + } +} + +public class Money { + private final BigDecimal amount; + private final Currency currency; + + public Money(BigDecimal amount, Currency currency) { + if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Amount cannot be null or negative"); + } + this.amount = amount.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_UP); + this.currency = Objects.requireNonNull(currency, "Currency cannot be null"); + } + + public BigDecimal getAmount() { return amount; } + public Currency getCurrency() { return currency; } + + public Money add(Money other) { + if (!currency.equals(other.currency)) { + throw new IllegalArgumentException("Cannot add different currencies"); + } + return new Money(amount.add(other.amount), currency); + } +} + +// Usage - type safety prevents errors +void processPayment(EmailAddress customerEmail, Money paymentAmount) { + // We know email is valid and amount is positive with proper currency + paymentService.charge(customerEmail.getValue(), paymentAmount); +} +``` + +**Bad example:** + +```java +// AVOID: Primitive obsession +void processPayment(String email, double amount, String currency) { + // Need to validate every time - error prone + if (email == null || !email.matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) { + throw new IllegalArgumentException("Invalid email"); + } + if (amount <= 0) { + throw new IllegalArgumentException("Amount must be positive"); + } + if (currency == null || currency.length() != 3) { + throw new IllegalArgumentException("Invalid currency code"); + } + + // Still risky - what if someone passes parameters in wrong order? + paymentService.charge(email, amount, currency); +} + +// Easy to make mistakes: +// processPayment("USD", 100.0, "john@example.com"); // Wrong parameter order! +// processPayment("invalid-email", -50.0, "XXX"); // Invalid values +``` + +## Rule 4: Leverage Generic Type Parameters + +Title: Create Flexible and Reusable Types (Responsive Typography) +Description: This rule promotes the use of generics to create flexible and reusable types and methods that can operate on objects of various types while maintaining type safety. This is akin to responsive typography that adapts to different screen sizes, as generics adapt to different data types. + +**Good example:** + +```java +// GOOD: Generic types adapt to different contexts +public class Repository { + private final Class entityClass; + private final EntityManager entityManager; + + public Repository(Class entityClass, EntityManager entityManager) { + this.entityClass = entityClass; + this.entityManager = entityManager; + } + + public Optional findById(Long id) { + T entity = entityManager.find(entityClass, id); + return Optional.ofNullable(entity); + } + + public List findAll() { + CriteriaQuery query = entityManager.getCriteriaBuilder() + .createQuery(entityClass); + query.select(query.from(entityClass)); + return entityManager.createQuery(query).getResultList(); + } + + public T save(T entity) { + if (entity.getId() == null) { + entityManager.persist(entity); + return entity; + } else { + return entityManager.merge(entity); + } + } + + public void delete(T entity) { + entityManager.remove(entity); + } +} + +// Usage for different entity types with full type safety +Repository customerRepo = new Repository<>(Customer.class, em); +Repository productRepo = new Repository<>(Product.class, em); + +// Type-safe operations +Optional customer = customerRepo.findById(1L); +List products = productRepo.findAll(); +``` + +**Bad example:** + +```java +// AVOID: Multiple similar classes with duplicated logic +public class CustomerRepository { + private final EntityManager entityManager; + + public CustomerRepository(EntityManager entityManager) { + this.entityManager = entityManager; + } + + public Optional findById(Long id) { + Customer customer = entityManager.find(Customer.class, id); + return Optional.ofNullable(customer); + } + + public List findAll() { + // Duplicated logic + CriteriaQuery query = entityManager.getCriteriaBuilder() + .createQuery(Customer.class); + query.select(query.from(Customer.class)); + return entityManager.createQuery(query).getResultList(); + } + + public Customer save(Customer customer) { + // Duplicated logic + if (customer.getId() == null) { + entityManager.persist(customer); + return customer; + } else { + return entityManager.merge(customer); + } + } +} + +public class ProductRepository { + // Exact same code but for Product - massive duplication! + private final EntityManager entityManager; + + public ProductRepository(EntityManager entityManager) { + this.entityManager = entityManager; + } + + public Optional findById(Long id) { + Product product = entityManager.find(Product.class, id); + return Optional.ofNullable(product); + } + + // ... more duplicated methods +} +``` + +## Rule 5: Use BigDecimal for Precision-Sensitive Calculations + +Title: Ensure Accuracy in Financial and Mathematical Operations +Description: This rule emphasizes using `java.math.BigDecimal` for calculations requiring high precision, especially with monetary values or any domain where rounding errors from binary floating-point arithmetic (like `float` or `double`) are unacceptable. Use consistent rounding modes and scale for predictable results. + +**Good example:** + +```java +// GOOD: Using BigDecimal for financial calculations +import java.math.BigDecimal; +import java.math.RoundingMode; + +public class FinancialCalculator { + private static final int CURRENCY_SCALE = 2; + private static final RoundingMode ROUNDING_MODE = RoundingMode.HALF_UP; + + public static BigDecimal calculateTotalPrice(BigDecimal itemPrice, BigDecimal taxRate, int quantity) { + validateInputs(itemPrice, taxRate, quantity); + + BigDecimal quantityDecimal = new BigDecimal(quantity); + BigDecimal subtotal = itemPrice.multiply(quantityDecimal); + BigDecimal taxAmount = subtotal.multiply(taxRate) + .setScale(CURRENCY_SCALE, ROUNDING_MODE); + + return subtotal.add(taxAmount).setScale(CURRENCY_SCALE, ROUNDING_MODE); + } + + public static BigDecimal calculateMonthlyPayment(BigDecimal principal, + BigDecimal annualRate, + int monthsTotal) { + validateInputs(principal, annualRate, monthsTotal); + + if (annualRate.compareTo(BigDecimal.ZERO) == 0) { + return principal.divide(new BigDecimal(monthsTotal), CURRENCY_SCALE, ROUNDING_MODE); + } + + BigDecimal monthlyRate = annualRate.divide(new BigDecimal("12"), 10, ROUNDING_MODE); + BigDecimal onePlusRate = BigDecimal.ONE.add(monthlyRate); + BigDecimal powResult = onePlusRate.pow(monthsTotal); + + BigDecimal numerator = principal.multiply(monthlyRate).multiply(powResult); + BigDecimal denominator = powResult.subtract(BigDecimal.ONE); + + return numerator.divide(denominator, CURRENCY_SCALE, ROUNDING_MODE); + } + + private static void validateInputs(BigDecimal amount, BigDecimal rate, int months) { + if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Amount must be non-negative"); + } + if (rate == null || rate.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Rate must be non-negative"); + } + if (months <= 0) { + throw new IllegalArgumentException("Months must be positive"); + } + } +} +``` + +**Bad example:** + +```java +// AVOID: Using double for financial calculations - precision issues +public class InaccurateFinancialCalculator { + public static double calculateTotalPrice(double itemPrice, double taxRate, int quantity) { + if (itemPrice < 0 || taxRate < 0 || quantity <= 0) { + throw new IllegalArgumentException("Invalid inputs"); + } + + double subtotal = itemPrice * quantity; + double taxAmount = subtotal * taxRate; + + // Precision issues! 0.1 + 0.2 != 0.3 in floating point + return subtotal + taxAmount; + } + + public static double calculateMonthlyPayment(double principal, double annualRate, int months) { + if (principal < 0 || annualRate < 0 || months <= 0) { + throw new IllegalArgumentException("Invalid inputs"); + } + + if (annualRate == 0) { + return principal / months; + } + + double monthlyRate = annualRate / 12; + double factor = Math.pow(1 + monthlyRate, months); + + // More precision issues with floating point arithmetic + return (principal * monthlyRate * factor) / (factor - 1); + } + + public static void main(String[] args) { + // This will demonstrate the precision problem + double result1 = calculateTotalPrice(19.99, 0.075, 3); + double result2 = calculateTotalPrice(29.99, 0.08, 2); + + System.out.println("Result 1: " + result1); // May show unexpected decimals + System.out.println("Result 2: " + result2); // May show unexpected decimals + + // Rounding manually is error-prone and inconsistent + System.out.println("Rounded 1: " + Math.round(result1 * 100.0) / 100.0); + System.out.println("Rounded 2: " + Math.round(result2 * 100.0) / 100.0); + } +} +``` diff --git a/spml/src/test/resources/123-java-general-guidelines.mdc b/spml/src/test/resources/123-java-general-guidelines.mdc new file mode 100644 index 00000000..daa97e30 --- /dev/null +++ b/spml/src/test/resources/123-java-general-guidelines.mdc @@ -0,0 +1,318 @@ +--- +description: Java General Guidelines +globs: *.java +alwaysApply: false +--- +# Java General Guidelines + +## System prompt characterization + +Role definition: You are a Senior software engineer with extensive experience in Java software development + +## Description + +This document outlines general Java coding guidelines covering fundamental aspects such as naming conventions for packages, classes, methods, variables, and constants; code formatting rules including indentation, line length, brace style, and whitespace usage; standards for organizing import statements; best practices for Javadoc documentation; and comprehensive error and exception handling with a strong focus on security, including avoiding sensitive information exposure, catching specific exceptions, and secure resource management. + +## Table of contents + +- Rule 1: Naming Conventions +- Rule 2: Formatting +- Rule 3: Import Statements +- Rule 4: Documentation Standards +- Rule 5: Comprehensive Error and Exception Handling + +## Rule 1: Naming Conventions + +Title: Follow Standard Java Naming Patterns +Description: Adhere to standard Java naming conventions for all code elements to promote intuitive, predictable, and easier to understand code navigation. + +**Good example:** + +```java +// GOOD: Proper naming conventions +package com.example.project.module; // Lowercase, reverse domain notation + +public class UserProfileService { // PascalCase for classes + public static final int MAX_LOGIN_ATTEMPTS = 3; // ALL_CAPS_SNAKE_CASE for constants + + private final UserRepository userRepository; // camelCase for variables + + public UserDTO getUserByUsername(String username) { // camelCase for methods + // ... implementation + } + + private boolean isValid(String input) { // Boolean methods with 'is', 'has', 'can' prefix + return input != null && !input.trim().isEmpty(); + } +} + +// Generic type parameters +public class Repository { // Single uppercase letter + // ... implementation +} +``` + +**Bad example:** + +```java +// AVOID: Poor naming conventions +package My_App_Services; // Uses underscores and wrong case + +public class userprofilesvc { // Not PascalCase + public static final int defaultpagesize = 20; // Not ALL_CAPS_SNAKE_CASE + + private UserRepository mUserRepository; // Hungarian notation (avoid) + + public UserDTO GetUser(String Username) { // Wrong case for method and parameter + // ... implementation + } +} +``` + +## Rule 2: Formatting + +Title: Apply Consistent Code Formatting +Description: Consistently apply formatting rules for indentation, line length, brace style, and whitespace to improve code readability and maintainability. + +**Good example:** + +```java +// GOOD: Proper formatting +public class FormattingExample { + private static final int MAX_RETRY_COUNT = 3; // Proper spacing around operators + + public void processData(String input) { + if (input == null || input.isEmpty()) { // K&R brace style + logger.warn("Input is null or empty"); + return; + } + + for (int i = 0; i < MAX_RETRY_COUNT; i++) { // Spaces after keywords and around operators + try { + performOperation(input); + break; + } catch (TemporaryException e) { + logger.debug("Retry attempt {}: {}", i + 1, e.getMessage()); + } + } + } +} +``` + +**Bad example:** + +```java +// AVOID: Poor formatting +public class BadFormattingExample{ + private static final int MAX_RETRY_COUNT=3;// No spaces + + public void processData(String input){ + if(input==null||input.isEmpty())// No spaces, missing braces + logger.warn("Input is null or empty"); + + for(int i=0;i\"'&]", ""); + + return sanitized.trim(); + } +} +``` + +**Bad example:** + +```java +// AVOID: Poor or missing documentation +public class StringHelper { + // No explanation of what it does or parameters + public boolean check(String s) { + return s == null || s.length() == 0; + } + + // Unclear method name and no documentation + public String fix(String s) { + return s.replaceAll("[<>]", ""); + } +} +``` + +## Rule 5: Comprehensive Error and Exception Handling + +Title: Implement Secure and Robust Error Management +Description: Implement robust error handling using specific exceptions, managing them at appropriate levels while preventing information leakage and ensuring proper resource cleanup. + +**Good example:** + +```java +// GOOD: Comprehensive error handling +public class SecureFileProcessor { + private static final Logger logger = LoggerFactory.getLogger(SecureFileProcessor.class); + + /** + * Reads file content safely with proper error handling. + * + * @param filePath The path to the file to read + * @return The file content + * @throws FileProcessingException if file cannot be processed + */ + public String readFile(Path filePath) throws FileProcessingException { + if (filePath == null) { + throw new IllegalArgumentException("File path cannot be null"); + } + + StringBuilder content = new StringBuilder(); + + // try-with-resources ensures proper resource cleanup + try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) { + String line; + while ((line = reader.readLine()) != null) { + content.append(line).append(System.lineSeparator()); + } + } catch (NoSuchFileException e) { + logger.warn("File not found: {}", filePath.getFileName()); + throw new FileProcessingException("Requested file not found", e); + } catch (AccessDeniedException e) { + logger.error("Access denied reading file: {}", filePath.getFileName()); + throw new FileProcessingException("Access denied", e); + } catch (IOException e) { + logger.error("IO error reading file: {}", filePath.getFileName(), e); + throw new FileProcessingException("Failed to read file", e); + } + + return content.toString(); + } +} +``` + +**Bad example:** + +```java +// AVOID: Poor error handling +public class UnsafeFileProcessor { + + public String readFile(String filePath) { + BufferedReader reader = null; + try { + reader = new BufferedReader(new FileReader(filePath)); + // ... reading logic + } catch (Exception e) { + // Swallowing exception - bad practice! + e.printStackTrace(); // Not using proper logging + return ""; // Hiding the problem + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + // Another swallowed exception + } + } + } + return null; + } +} +``` diff --git a/spml/src/test/resources/124-java-secure-coding.mdc b/spml/src/test/resources/124-java-secure-coding.mdc new file mode 100644 index 00000000..993249c0 --- /dev/null +++ b/spml/src/test/resources/124-java-secure-coding.mdc @@ -0,0 +1,633 @@ +--- +description: Java Secure coding guidelines +globs: *.java +alwaysApply: false +--- +# Java Secure coding guidelines + +## System prompt characterization + +Role definition: You are a Senior software engineer with extensive experience in Java software development + +## Description + +This document provides essential Java secure coding guidelines, focusing on five key areas: validating all untrusted inputs to prevent attacks like injection and path traversal; protecting against injection attacks (e.g., SQL injection) by using parameterized queries or prepared statements; minimizing the attack surface by adhering to the principle of least privilege and reducing exposure; employing strong, current cryptographic algorithms for hashing, encryption, and digital signatures while avoiding deprecated ones; and handling exceptions securely by avoiding the exposure of sensitive information in error messages to users and logging detailed, non-sensitive diagnostic information for developers. + +## Table of contents + +- Rule 1: Input Validation +- Rule 2: Protect Against Injection Attacks +- Rule 3: Minimize Attack Surface +- Rule 4: Use Strong Cryptography +- Rule 5: Handle Exceptions Securely + +## Rule 1: Input Validation + +Title: Validate All Untrusted Inputs +Description: Always validate and sanitize data received from untrusted sources (users, network, files, etc.) before processing. This helps prevent various attacks like injection, path traversal, and buffer overflows. Validation should check for type, length, format, and range. + +**Good example:** + +```java +// GOOD: Comprehensive input validation +import java.util.Objects; +import java.util.regex.Pattern; + +public class SecureInputValidator { + private static final Pattern USERNAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]{3,16}$"); + private static final int MAX_AGE = 120; + private static final int MIN_AGE = 0; + + public void processUserData(String username, String ageString) { + // Validate username format + if (Objects.isNull(username) || !USERNAME_PATTERN.matcher(username).matches()) { + throw new IllegalArgumentException("Invalid username format. Must be 3-16 alphanumeric characters or underscores."); + } + + // Validate and parse age + int age; + try { + age = Integer.parseInt(ageString); + if (age < MIN_AGE || age > MAX_AGE) { + throw new IllegalArgumentException("Age must be between " + MIN_AGE + " and " + MAX_AGE); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid age format. Must be a valid integer.", e); + } + + // Input is now validated and safe to process + System.out.println("Processing validated user: " + username + ", age: " + age); + } + + public String sanitizeFilePath(String userPath) { + if (userPath == null) { + throw new IllegalArgumentException("File path cannot be null"); + } + + // Prevent path traversal attacks + String sanitized = userPath.replaceAll("\\.\\.", "").replaceAll("/", ""); + + // Additional validation + if (sanitized.length() > 255) { + throw new IllegalArgumentException("File path too long"); + } + + return sanitized; + } +} +``` + +**Bad example:** + +```java +// AVOID: No input validation +public class UnsafeInputProcessor { + public void processUserData(String username, String ageString) { + // Directly using input without validation - DANGEROUS! + int age = Integer.parseInt(ageString); // Can throw NumberFormatException + + // No checks for malicious username strings + System.out.println("Processing user: " + username + ", age: " + age); + + // This could lead to issues if username contains scripts or ageString is not an integer + } + + public String loadFile(String userPath) { + // VULNERABLE: No validation allows path traversal attacks + // User could pass "../../etc/passwd" to access sensitive files + return readFileContent(userPath); + } +} +``` + +## Rule 2: Protect Against Injection Attacks + +Title: Use Parameterized Queries and Safe APIs +Description: To prevent SQL Injection and other injection attacks, always use parameterized queries (PreparedStatements in JDBC) or an ORM that handles this automatically. Never concatenate user input directly into SQL queries, OS commands, or other executable statements. + +**Good example:** + +```java +// GOOD: Using PreparedStatement to prevent SQL Injection +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +public class SecureDataAccess { + private static final String DB_URL = "jdbc:h2:mem:testdb"; + private static final String USER = "sa"; + private static final String PASS = ""; + + public List getOrdersByCustomerId(String customerId) throws SQLException { + // Safe parameterized query + String query = "SELECT order_id, customer_id, amount FROM orders WHERE customer_id = ?"; + List orders = new ArrayList<>(); + + try (Connection con = DriverManager.getConnection(DB_URL, USER, PASS); + PreparedStatement pstmt = con.prepareStatement(query)) { + + pstmt.setString(1, customerId); // Parameter is safely bound + ResultSet rs = pstmt.executeQuery(); + + while (rs.next()) { + Order order = new Order(); + order.setOrderId(rs.getString("order_id")); + order.setCustomerId(rs.getString("customer_id")); + order.setAmount(rs.getBigDecimal("amount")); + orders.add(order); + } + } + + return orders; + } + + public void updateCustomerEmail(String customerId, String newEmail) throws SQLException { + String updateQuery = "UPDATE customers SET email = ? WHERE customer_id = ?"; + + try (Connection con = DriverManager.getConnection(DB_URL, USER, PASS); + PreparedStatement pstmt = con.prepareStatement(updateQuery)) { + + pstmt.setString(1, newEmail); + pstmt.setString(2, customerId); + pstmt.executeUpdate(); + } + } +} +``` + +**Bad example:** + +```java +// AVOID: Vulnerable to SQL Injection +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +public class VulnerableDataAccess { + private static final String DB_URL = "jdbc:h2:mem:testdb"; + private static final String USER = "sa"; + private static final String PASS = ""; + + public List getOrdersByCustomerId(String customerId) throws SQLException { + // DANGEROUS: User input directly concatenated into SQL query + String query = "SELECT order_id, customer_id, amount FROM orders WHERE customer_id = '" + customerId + "'"; + List orders = new ArrayList<>(); + + try (Connection con = DriverManager.getConnection(DB_URL, USER, PASS); + Statement stmt = con.createStatement()) { + + // User could pass: "'; DROP TABLE orders; --" to execute malicious SQL + ResultSet rs = stmt.executeQuery(query); + + while (rs.next()) { + // Process results... + } + } + + return orders; + } + + public void executeCommand(String userCommand) { + // EXTREMELY DANGEROUS: Command injection vulnerability + String command = "ls " + userCommand; // User could inject "; rm -rf /" + try { + Runtime.getRuntime().exec(command); + } catch (Exception e) { + // Handle exception + } + } +} +``` + +## Rule 3: Minimize Attack Surface + +Title: Apply Principle of Least Privilege +Description: Grant only necessary permissions to code and users. Avoid running processes with excessive privileges. Expose only essential functionality and network ports. Regularly review and remove unused features, libraries, and accounts. + +**Good example:** + +```java +// GOOD: Minimal privileges and controlled access +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.Principal; +import java.util.Set; + +public class SecureFileManager { + private static final Set ALLOWED_EXTENSIONS = Set.of(".txt", ".log", ".json"); + private static final Path SAFE_DIRECTORY = Paths.get("/app/data/uploads"); + + public String readUserFile(String filename, Principal user) throws SecurityException, IOException { + // Check user permissions + if (!hasReadPermission(user, filename)) { + throw new SecurityException("User does not have permission to read this file"); + } + + // Validate file extension + String extension = getFileExtension(filename); + if (!ALLOWED_EXTENSIONS.contains(extension)) { + throw new SecurityException("File type not allowed: " + extension); + } + + // Ensure file is within safe directory + Path filePath = SAFE_DIRECTORY.resolve(filename).normalize(); + if (!filePath.startsWith(SAFE_DIRECTORY)) { + throw new SecurityException("File access outside allowed directory"); + } + + // Check file size limits + if (Files.size(filePath) > 1024 * 1024) { // 1MB limit + throw new SecurityException("File too large"); + } + + return Files.readString(filePath); + } + + private boolean hasReadPermission(Principal user, String filename) { + // Implement proper authorization logic + return user != null && user.getName() != null; + } + + private String getFileExtension(String filename) { + int lastDot = filename.lastIndexOf('.'); + return lastDot > 0 ? filename.substring(lastDot) : ""; + } +} + +// Example of interface segregation - expose only necessary methods +public interface UserService { + User findById(Long id); + void updateProfile(Long id, UserProfile profile); + // Don't expose administrative methods to regular users +} + +public interface AdminService extends UserService { + void deleteUser(Long id); + List getAllUsers(); + void resetPassword(Long id); +} +``` + +**Bad example:** + +```java +// AVOID: Excessive privileges and exposure +import java.io.File; +import java.nio.file.Files; + +public class UnsafeFileManager { + + // BAD: No access controls or validations + public String readAnyFile(String filename) throws IOException { + // DANGEROUS: Can read any file on the system + File file = new File(filename); + return Files.readString(file.toPath()); + } + + // BAD: Exposing dangerous functionality + public void executeSystemCommand(String command) throws IOException { + // EXTREMELY DANGEROUS: Allows arbitrary command execution + Runtime.getRuntime().exec(command); + } + + // BAD: Administrative functions mixed with user functions + public class UserController { + public void updateProfile(User user) { /* ... */ } + public void deleteAllUsers() { /* ... */ } // Should not be here + public void resetDatabase() { /* ... */ } // Extremely dangerous + public void viewSystemLogs() { /* ... */ } // Sensitive operation + } +} + +// BAD: Running with excessive privileges +// Starting application as root/administrator user +// Using database accounts with DBA privileges for regular operations +// Exposing debug endpoints in production +// Having default passwords or credentials +``` + +## Rule 4: Use Strong Cryptography + +Title: Employ Current and Robust Cryptographic Algorithms +Description: Use well-vetted, industry-standard cryptographic libraries and algorithms for hashing, encryption, and digital signatures. Avoid deprecated or weak algorithms (e.g., MD5, SHA1 for passwords, DES). Keep cryptographic keys secure and manage them properly. + +**Good example:** + +```java +// GOOD: Strong cryptographic practices +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import java.security.SecureRandom; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +public class SecureCryptoUtils { + private static final String AES_ALGORITHM = "AES"; + private static final String AES_TRANSFORMATION = "AES/GCM/NoPadding"; + private static final int GCM_TAG_LENGTH = 16; + private static final int GCM_IV_LENGTH = 12; + + private final PasswordEncoder passwordEncoder; + private final SecureRandom secureRandom; + + public SecureCryptoUtils() { + this.passwordEncoder = new BCryptPasswordEncoder(12); // Strong cost factor + this.secureRandom = new SecureRandom(); + } + + /** + * Generates a secure AES-256 key + */ + public SecretKey generateAESKey() throws Exception { + KeyGenerator keyGen = KeyGenerator.getInstance(AES_ALGORITHM); + keyGen.init(256, secureRandom); // Use AES-256 + return keyGen.generateKey(); + } + + /** + * Encrypts data using AES-GCM (authenticated encryption) + */ + public EncryptedData encrypt(String plaintext, SecretKey key) throws Exception { + Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION); + + // Generate random IV + byte[] iv = new byte[GCM_IV_LENGTH]; + secureRandom.nextBytes(iv); + + GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv); + cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec); + + byte[] encryptedData = cipher.doFinal(plaintext.getBytes("UTF-8")); + + return new EncryptedData(encryptedData, iv); + } + + /** + * Hashes password using BCrypt with salt + */ + public String hashPassword(String plainTextPassword) { + return passwordEncoder.encode(plainTextPassword); + } + + /** + * Verifies password against BCrypt hash + */ + public boolean verifyPassword(String plainTextPassword, String hashedPassword) { + return passwordEncoder.matches(plainTextPassword, hashedPassword); + } + + /** + * Generates cryptographically secure random token + */ + public String generateSecureToken(int length) { + byte[] tokenBytes = new byte[length]; + secureRandom.nextBytes(tokenBytes); + return Base64.getEncoder().encodeToString(tokenBytes); + } + + public static class EncryptedData { + private final byte[] ciphertext; + private final byte[] iv; + + public EncryptedData(byte[] ciphertext, byte[] iv) { + this.ciphertext = ciphertext; + this.iv = iv; + } + + // Getters... + } +} +``` + +**Bad example:** + +```java +// AVOID: Weak cryptographic practices +import java.security.MessageDigest; +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.util.Random; + +public class WeakCryptoUtils { + + /** + * BAD: Using MD5 for password hashing + */ + public String hashPasswordMD5(String password) throws Exception { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(password.getBytes()); + byte[] digest = md.digest(); + return bytesToHex(digest); + } + + /** + * BAD: Using SHA1 for password hashing (without salt) + */ + public String hashPasswordSHA1(String password) throws Exception { + MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); + sha1.update(password.getBytes()); + return bytesToHex(sha1.digest()); + } + + /** + * BAD: Using DES encryption (weak algorithm) + */ + public byte[] encryptDES(String plaintext, String password) throws Exception { + SecretKeySpec key = new SecretKeySpec(password.getBytes(), "DES"); + Cipher cipher = Cipher.getInstance("DES"); + cipher.init(Cipher.ENCRYPT_MODE, key); + return cipher.doFinal(plaintext.getBytes()); + } + + /** + * BAD: Hardcoded encryption key + */ + private static final String HARDCODED_KEY = "mySecretKey123"; + + public byte[] encryptWithHardcodedKey(String data) throws Exception { + // TERRIBLE: Hardcoded key in source code + SecretKeySpec key = new SecretKeySpec(HARDCODED_KEY.getBytes(), "AES"); + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.ENCRYPT_MODE, key); + return cipher.doFinal(data.getBytes()); + } + + /** + * BAD: Using weak random number generator + */ + public String generateWeakToken() { + Random random = new Random(); // NOT cryptographically secure + return String.valueOf(random.nextLong()); + } + + private String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02x", b)); + } + return result.toString(); + } +} + +// Additional bad practices: +// - Storing passwords in plain text +// - Using ECB mode for encryption +// - Not using authenticated encryption +// - Reusing IVs/nonces +// - Using weak key derivation functions +``` + +## Rule 5: Handle Exceptions Securely + +Title: Avoid Exposing Sensitive Information +Description: Catch exceptions appropriately, but do not reveal sensitive system details or stack traces to users in production. Log detailed error information server-side for debugging, but provide generic error messages to the client. + +**Good example:** + +```java +// GOOD: Secure exception handling +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class SecureExceptionHandler { + private static final Logger logger = LoggerFactory.getLogger(SecureExceptionHandler.class); + + public void performSensitiveOperation(String userId, String sensitiveData) + throws ServiceException { + try { + validateUser(userId); + processData(sensitiveData); + + } catch (ValidationException e) { + // Log detailed error for developers (no sensitive data) + logger.warn("Validation failed for user: {}. Error: {}", + sanitizeForLogging(userId), e.getErrorCode()); + + // Provide generic error message to user + throw new ServiceException("Invalid input provided. Please check your data.", e); + + } catch (DatabaseException e) { + // Log technical details for debugging + logger.error("Database error during operation for user: {}. Query: {}", + sanitizeForLogging(userId), e.getQueryId(), e); + + // Generic message to user - don't expose database details + throw new ServiceException("Service temporarily unavailable. Please try again later.", e); + + } catch (SecurityException e) { + // Security incidents need special logging + logger.error("SECURITY: Unauthorized access attempt by user: {}. Details: {}", + sanitizeForLogging(userId), e.getMessage()); + + // Don't give attackers information about what they're doing wrong + throw new ServiceException("Access denied.", e); + + } catch (Exception e) { + // Catch-all for unexpected errors + logger.error("Unexpected error in sensitive operation for user: {}", + sanitizeForLogging(userId), e); + + throw new ServiceException("An unexpected error occurred. Please contact support.", e); + } + } + + /** + * Sanitizes user input for safe logging + */ + private String sanitizeForLogging(String input) { + if (input == null) return "null"; + + // Remove or mask potentially sensitive characters + return input.replaceAll("[^a-zA-Z0-9-_]", "*"); + } + + public void handleWebRequest(HttpServletResponse response, String userInput) { + try { + processUserInput(userInput); + response.setStatus(200); + + } catch (ServiceException e) { + // Log the exception with context + logger.error("Service error processing request: {}", e.getMessage(), e); + + try { + // Return generic error to client + response.setStatus(500); + response.getWriter().write("{\"error\":\"Internal server error\"}"); + } catch (IOException ioException) { + logger.error("Failed to write error response", ioException); + } + } + } +} +``` + +**Bad example:** + +```java +// AVOID: Insecure exception handling +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.sql.SQLException; + +public class InsecureExceptionHandler { + + public void performSensitiveOperation(String userId, String creditCardNumber) { + try { + processPayment(userId, creditCardNumber); + + } catch (SQLException e) { + // BAD: Exposing SQL error details to user + throw new RuntimeException("Database error: " + e.toString() + + " Query: " + e.getMessage()); + + } catch (SecurityException e) { + // BAD: Revealing security details to potential attacker + throw new RuntimeException("Security check failed: " + e.getMessage() + + " User role: admin required, current: " + getCurrentUserRole()); + + } catch (Exception e) { + // BAD: Generic catch that might expose stack traces + e.printStackTrace(); // Prints to stderr, might be visible to users + throw new RuntimeException("Error processing payment for card: " + + creditCardNumber, e); // Leaks sensitive data! + } + } + + public void handleWebRequest(HttpServletResponse response, String userInput) { + try { + processUserInput(userInput); + + } catch (Exception e) { + try { + // BAD: Exposing full stack trace to client + response.getWriter().println("Error: " + e.toString()); + e.printStackTrace(response.getWriter()); // NEVER do this! + + // BAD: Revealing internal paths and system information + response.getWriter().println("System details: " + System.getProperty("user.dir")); + response.getWriter().println("Java version: " + System.getProperty("java.version")); + + } catch (IOException ioException) { + // More problems - nested exception handling + ioException.printStackTrace(); + } + } + } + + public void authenticateUser(String username, String password) { + try { + performAuthentication(username, password); + } catch (Exception e) { + // BAD: Logging sensitive information + System.out.println("Authentication failed for user: " + username + + " with password: " + password + " Error: " + e.getMessage()); + } + } +} +``` diff --git a/spml/src/test/resources/125-java-concurrency.mdc b/spml/src/test/resources/125-java-concurrency.mdc new file mode 100644 index 00000000..51ae9b40 --- /dev/null +++ b/spml/src/test/resources/125-java-concurrency.mdc @@ -0,0 +1,873 @@ +--- +description: Java rules for Concurrency objects +globs: *.java +alwaysApply: false +--- +# Java rules for Concurrency objects + +## System prompt characterization + +Role definition: You are a Senior software engineer with extensive experience in Java software development + +## Description + +Effective Java concurrency relies on understanding thread safety fundamentals, using `java.util.concurrent` utilities, and managing thread pools with `ExecutorService`. Key practices include implementing concurrent design patterns like Producer-Consumer, leveraging `CompletableFuture` for asynchronous tasks, and ensuring thread safety through immutability and safe publication. Performance aspects like lock contention and memory consistency must be considered. Thorough testing, including stress tests and thread dump analysis, is crucial. Modern Java offers virtual threads for enhanced scalability, structured concurrency for simplified task management, and scoped values for safer thread-shared data as alternatives to thread-locals. + +## Table of contents + +- Rule 1: Thread Safety Fundamentals +- Rule 2: Thread Pool Management +- Rule 3: Concurrent Design Patterns +- Rule 4: Asynchronous Programming with CompletableFuture +- Rule 5: Embrace Virtual Threads for Enhanced Scalability + +## Rule 1: Thread Safety Fundamentals + +Title: Master Core Thread Safety Concepts +Description: Understand and correctly apply core concepts such as synchronization, atomic operations, thread-safe collections, immutability, and the Java Memory Model to ensure data integrity and prevent race conditions or deadlocks. + +**Good example:** + +```java +// GOOD: Proper thread safety using concurrent collections and atomics +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class ThreadSafeCounter { + // Preferred concurrent collections + private final Map concurrentMap = new ConcurrentHashMap<>(); + private final Queue taskQueue = new ConcurrentLinkedQueue<>(); + private final BlockingQueue eventQueue = new LinkedBlockingQueue<>(); + + // Atomic variables for lock-free operations + private final AtomicInteger counter = new AtomicInteger(0); + private final AtomicReference status = new AtomicReference<>("INIT"); + + // Thread-local storage + private static final ThreadLocal userContext = + ThreadLocal.withInitial(() -> "default"); + + // Using ReentrantLock for complex synchronization + private final ReentrantLock lock = new ReentrantLock(); + private int sharedResource = 0; + + public void incrementCounter() { + // Atomic operation - thread-safe without locks + int newValue = counter.incrementAndGet(); + concurrentMap.put("lastCount", String.valueOf(newValue)); + } + + public void updateSharedResource(int delta) { + lock.lock(); + try { + sharedResource += delta; + System.out.println("Updated resource: " + sharedResource); + } finally { + lock.unlock(); // Always unlock in finally block + } + } + + // Using ReadWriteLock for better performance with frequent reads + private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); + private String sharedData = "Initial Data"; + + public String readData() { + rwLock.readLock().lock(); + try { + return sharedData; + } finally { + rwLock.readLock().unlock(); + } + } + + public void writeData(String data) { + rwLock.writeLock().lock(); + try { + sharedData = data; + } finally { + rwLock.writeLock().unlock(); + } + } +} +``` + +**Bad example:** + +```java +// AVOID: Poor thread safety practices +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class UnsafeCounter { + // BAD: Using non-thread-safe collections + private Map unsafeMap = new HashMap<>(); + private List unsafeList = new ArrayList<>(); + + // BAD: Using plain int without synchronization + private int counter = 0; + private String status = "INIT"; + + public void incrementCounter() { + // RACE CONDITION: Multiple threads can read same value + counter++; // Not atomic - can lose updates + unsafeMap.put("lastCount", String.valueOf(counter)); // Can corrupt map + } + + // BAD: Inconsistent synchronization + public synchronized void updateCounter(int value) { + counter = value; // Synchronized + } + + public int getCounter() { + return counter; // NOT synchronized - can read stale value + } + + // BAD: Synchronizing on mutable object + private String lockObject = "lock"; + + public void badSynchronization() { + synchronized (lockObject) { // WRONG: string can be changed + // Critical section + } + lockObject = "newLock"; // Now synchronization is broken! + } +} +``` + +## Rule 2: Thread Pool Management + +Title: Manage Thread Pools Effectively with ExecutorService +Description: Utilize ExecutorService for robust thread management. Choose appropriate thread pool implementations, configure them properly, and implement graceful shutdown procedures. + +**Good example:** + +```java +// GOOD: Proper thread pool management +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +public class ThreadPoolManager { + private final ExecutorService fixedPool; + private final ScheduledExecutorService scheduler; + private final ThreadPoolExecutor customPool; + + public ThreadPoolManager() { + // Fixed thread pool with named threads + this.fixedPool = Executors.newFixedThreadPool( + Runtime.getRuntime().availableProcessors(), + new CustomThreadFactory("worker") + ); + + // Scheduled thread pool for periodic tasks + this.scheduler = Executors.newScheduledThreadPool( + 1, + new CustomThreadFactory("scheduler") + ); + + // Custom thread pool with fine-grained control + this.customPool = new ThreadPoolExecutor( + 2, // core pool size + 4, // maximum pool size + 60L, // keep alive time + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(100), // bounded queue + new CustomThreadFactory("custom"), + new ThreadPoolExecutor.CallerRunsPolicy() // rejection policy + ); + } + + public void submitTask(Runnable task) { + fixedPool.submit(task); + } + + public void schedulePeriodicTask(Runnable task, long period) { + scheduler.scheduleAtFixedRate(task, 0, period, TimeUnit.SECONDS); + } + + public void shutdown() { + shutdownExecutorService(fixedPool, "FixedPool"); + shutdownExecutorService(scheduler, "Scheduler"); + shutdownExecutorService(customPool, "CustomPool"); + } + + private void shutdownExecutorService(ExecutorService executor, String name) { + executor.shutdown(); // Disable new tasks + try { + // Wait for existing tasks to terminate + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { + executor.shutdownNow(); // Cancel currently executing tasks + + // Wait for tasks to respond to being cancelled + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { + System.err.println("Pool " + name + " did not terminate"); + } + } + } catch (InterruptedException ie) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + // Custom thread factory for better thread naming and error handling + private static class CustomThreadFactory implements ThreadFactory { + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + CustomThreadFactory(String namePrefix) { + this.namePrefix = namePrefix + "-thread-"; + } + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement()); + t.setDaemon(false); + t.setUncaughtExceptionHandler((thread, ex) -> { + System.err.println("Thread " + thread.getName() + " threw exception: " + ex); + }); + return t; + } + } +} +``` + +**Bad example:** + +```java +// AVOID: Poor thread pool management +import java.util.concurrent.Executors; +import java.util.concurrent.ExecutorService; + +public class PoorThreadPoolManager { + // BAD: Unbounded thread pools can cause resource exhaustion + private ExecutorService cachedPool = Executors.newCachedThreadPool(); + + // BAD: Single thread with unbounded queue + private ExecutorService singleThread = Executors.newSingleThreadExecutor(); + + public void submitTask(Runnable task) { + // BAD: No error handling or resource management + cachedPool.submit(task); + } + + public void submitManyTasks() { + for (int i = 0; i < 10000; i++) { + // BAD: Can overwhelm the system + cachedPool.submit(() -> { + try { + Thread.sleep(10000); // Long-running task + } catch (InterruptedException e) { + // BAD: Ignoring interruption + } + }); + } + } + + // BAD: No proper shutdown + public void shutdown() { + cachedPool.shutdown(); // What if tasks don't finish? + singleThread.shutdown(); + // No waiting for termination + // No handling of interrupted exception + // No forced shutdown if graceful shutdown fails + } + + // BAD: Creating new thread for each task + public void executeTask(Runnable task) { + new Thread(task).start(); // Expensive and uncontrolled + } + + // BAD: No thread naming or error handling + // Default thread names are not descriptive + // Uncaught exceptions terminate threads silently +} +``` + +## Rule 3: Concurrent Design Patterns + +Title: Implement Producer-Consumer and Publish-Subscribe +Description: Leverage established patterns like Producer-Consumer and Publish-Subscribe to structure concurrent applications effectively, promoting decoupling, scalability, and maintainability. + +**Good example:** + +```java +// GOOD: Producer-Consumer pattern with BlockingQueue +import java.util.concurrent.*; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +// Producer-Consumer implementation +public class ProducerConsumerExample { + private final BlockingQueue queue = new LinkedBlockingQueue<>(100); + private final ExecutorService executor = Executors.newFixedThreadPool(4); + private volatile boolean running = true; + + public void startProcessing() { + // Start multiple consumers + for (int i = 0; i < 2; i++) { + executor.submit(this::consumer); + } + } + + public void produce(Task task) throws InterruptedException { + if (running) { + queue.put(task); // Blocks if queue is full + } + } + + private void consumer() { + while (running || !queue.isEmpty()) { + try { + Task task = queue.poll(1, TimeUnit.SECONDS); + if (task != null) { + processTask(task); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + + private void processTask(Task task) { + System.out.println("Processing: " + task + " on " + Thread.currentThread().getName()); + // Simulate work + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public void shutdown() { + running = false; + executor.shutdown(); + } + + private static class Task { + private final String data; + public Task(String data) { this.data = data; } + @Override public String toString() { return "Task(" + data + ")"; } + } +} + +// Publish-Subscribe implementation +public class EventBus { + private final ConcurrentHashMap> listeners = new ConcurrentHashMap<>(); + private final ExecutorService notificationExecutor = ForkJoinPool.commonPool(); + + public void subscribe(String topic, EventListener listener) { + listeners.computeIfAbsent(topic, k -> ConcurrentHashMap.newKeySet()) + .add(listener); + } + + public void unsubscribe(String topic, EventListener listener) { + Set topicListeners = listeners.get(topic); + if (topicListeners != null) { + topicListeners.remove(listener); + } + } + + public void publish(String topic, Event event) { + Set topicListeners = listeners.get(topic); + if (topicListeners != null && !topicListeners.isEmpty()) { + // Notify listeners asynchronously + topicListeners.forEach(listener -> + notificationExecutor.submit(() -> { + try { + listener.onEvent(event); + } catch (Exception e) { + System.err.println("Error notifying listener: " + e.getMessage()); + } + }) + ); + } + } + + private static class Event { + private final String data; + public Event(String data) { this.data = data; } + @Override public String toString() { return "Event(" + data + ")"; } + } + + @FunctionalInterface + private interface EventListener { + void onEvent(Event event); + } +} +``` + +**Bad example:** + +```java +// AVOID: Poor concurrent pattern implementation +import java.util.*; + +public class BadProducerConsumer { + // BAD: Using non-thread-safe collection + private List tasks = new ArrayList<>(); + private boolean running = true; + + public void produce(String task) { + // RACE CONDITION: Multiple producers can corrupt the list + synchronized (this) { + tasks.add(task); + notify(); // BAD: Should use notifyAll() + } + } + + public void consume() { + while (running) { + String task = null; + synchronized (this) { + while (tasks.isEmpty() && running) { + try { + wait(); // BAD: Can miss notifications + } catch (InterruptedException e) { + // BAD: Not handling interruption properly + return; + } + } + if (!tasks.isEmpty()) { + task = tasks.remove(0); // BAD: Inefficient removal from front + } + } + + if (task != null) { + // BAD: Processing inside synchronized block would be even worse + processTask(task); + } + } + } + + // BAD: No proper shutdown mechanism + public void stop() { + running = false; // Consumers might not wake up + } +} + +// BAD: Synchronous event handling +public class BadEventBus { + private Map> listeners = new HashMap<>(); + + public void subscribe(String topic, EventListener listener) { + // BAD: Not thread-safe + listeners.computeIfAbsent(topic, k -> new ArrayList<>()).add(listener); + } + + public void publish(String topic, String event) { + List topicListeners = listeners.get(topic); + if (topicListeners != null) { + // BAD: Synchronous notification blocks publisher + for (EventListener listener : topicListeners) { + try { + listener.onEvent(event); + } catch (Exception e) { + // BAD: One failing listener affects others + throw new RuntimeException("Event handling failed", e); + } + } + } + } +} +``` + +## Rule 4: Asynchronous Programming with CompletableFuture + +Title: Compose Non-blocking Asynchronous Operations +Description: Employ CompletableFuture to compose and manage asynchronous computations in a non-blocking way. Chain dependent tasks, combine results from multiple futures, and handle exceptions gracefully. + +**Good example:** + +```java +// GOOD: CompletableFuture for asynchronous processing +import java.util.concurrent.*; +import java.util.List; +import java.util.stream.Collectors; + +public class AsyncService { + private final ExecutorService customExecutor = Executors.newFixedThreadPool(4); + + public CompletableFuture processDataAsync(String input) { + return CompletableFuture + .supplyAsync(() -> validateInput(input), customExecutor) + .thenApplyAsync(this::transformData, customExecutor) + .thenApply(this::formatResult) + .exceptionally(this::handleError) + .whenComplete((result, ex) -> { + if (ex != null) { + System.err.println("Processing failed for: " + input); + } else { + System.out.println("Successfully processed: " + input); + } + }); + } + + public CompletableFuture> processMultipleAsync(List inputs) { + List> futures = inputs.stream() + .map(this::processDataAsync) + .collect(Collectors.toList()); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList())) + .exceptionally(ex -> { + System.err.println("Batch processing failed: " + ex.getMessage()); + return List.of("ERROR"); + }); + } + + public CompletableFuture combineResults(String input1, String input2) { + CompletableFuture future1 = processDataAsync(input1); + CompletableFuture future2 = processDataAsync(input2); + + return future1.thenCombine(future2, (result1, result2) -> + "Combined: " + result1 + " + " + result2); + } + + public CompletableFuture getFirstSuccessful(List inputs) { + CompletableFuture[] futures = inputs.stream() + .map(this::processDataAsync) + .toArray(CompletableFuture[]::new); + + return CompletableFuture.anyOf(futures) + .thenApply(result -> (String) result); + } + + private String validateInput(String input) { + if (input == null || input.trim().isEmpty()) { + throw new IllegalArgumentException("Input cannot be null or empty"); + } + // Simulate validation work + try { Thread.sleep(100); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + return input.trim(); + } + + private String transformData(String input) { + // Simulate transformation work + try { Thread.sleep(200); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + return "transformed_" + input; + } + + private String formatResult(String input) { + return "[" + input + "]"; + } + + private String handleError(Throwable throwable) { + System.err.println("Error occurred: " + throwable.getMessage()); + return "ERROR: " + throwable.getClass().getSimpleName(); + } + + public void shutdown() { + customExecutor.shutdown(); + try { + if (!customExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + customExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + customExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } +} +``` + +**Bad example:** + +```java +// AVOID: Blocking operations and poor error handling +import java.util.concurrent.*; +import java.util.List; +import java.util.ArrayList; + +public class BadAsyncService { + + public String processDataBlocking(String input) { + // BAD: Using CompletableFuture but blocking immediately + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + return processInput(input); + }); + + try { + return future.get(); // BLOCKING! Defeats the purpose + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public List processMultipleBlocking(List inputs) { + List results = new ArrayList<>(); + + // BAD: Sequential processing instead of parallel + for (String input : inputs) { + CompletableFuture future = CompletableFuture.supplyAsync(() -> + processInput(input)); + + try { + results.add(future.get()); // BLOCKING in loop + } catch (Exception e) { + // BAD: One failure stops everything + throw new RuntimeException("Processing failed", e); + } + } + + return results; + } + + public CompletableFuture badErrorHandling(String input) { + return CompletableFuture.supplyAsync(() -> { + if ("fail".equals(input)) { + throw new RuntimeException("Simulated failure"); + } + return processInput(input); + }); + // BAD: No error handling - exceptions will propagate + } + + public void badChaining(String input) { + // BAD: Not chaining properly, creating nested futures + CompletableFuture> nestedFuture = + CompletableFuture.supplyAsync(() -> { + return CompletableFuture.supplyAsync(() -> { + return processInput(input); + }); + }); + + // Now you have a nested CompletableFuture - hard to work with + } + + // BAD: Resource leak - no executor shutdown + private final ExecutorService executor = Executors.newFixedThreadPool(10); + + public CompletableFuture processWithLeakedExecutor(String input) { + return CompletableFuture.supplyAsync(() -> processInput(input), executor); + // BAD: Executor never gets shut down + } + + private String processInput(String input) { + try { + Thread.sleep(1000); // Simulate work + } catch (InterruptedException e) { + // BAD: Not handling interruption properly + } + return "processed_" + input; + } +} +``` + +## Rule 5: Embrace Virtual Threads for Enhanced Scalability + +Title: Use Virtual Threads for I/O-bound Tasks +Description: Leverage virtual threads (Project Loom) for I/O-bound tasks to dramatically increase scalability with minimal resource overhead. Avoid pooling virtual threads and use structured concurrency where appropriate. + +**Good example:** + +```java +// GOOD: Using virtual threads for scalable I/O operations +import java.util.concurrent.*; +import java.util.List; +import java.util.stream.IntStream; + +public class VirtualThreadExample { + + // Use virtual thread executor for I/O-bound tasks + private final ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor(); + + public void handleManyIORequests() { + List> futures = IntStream.range(0, 10000) + .mapToObj(i -> virtualExecutor.submit(() -> performIOOperation("task-" + i))) + .toList(); + + // Collect results + futures.forEach(future -> { + try { + String result = future.get(); + System.out.println("Completed: " + result); + } catch (Exception e) { + System.err.println("Task failed: " + e.getMessage()); + } + }); + } + + // Virtual threads are perfect for blocking I/O operations + private String performIOOperation(String taskId) { + try { + // Simulate I/O operation (database call, web request, etc.) + Thread.sleep(1000); // This would block a platform thread + return "Result for " + taskId; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return "Interrupted: " + taskId; + } + } + + // Using scoped values with virtual threads (Java 20+) + private static final ScopedValue USER_ID = ScopedValue.newInstance(); + + public void processWithScopedValue(String userId, List tasks) { + // Run with scoped value + ScopedValue.where(USER_ID, userId) + .run(() -> { + tasks.parallelStream().forEach(task -> { + virtualExecutor.submit(() -> { + // Access scoped value safely + String currentUserId = USER_ID.get(); + performTaskForUser(currentUserId, task); + }); + }); + }); + } + + private void performTaskForUser(String userId, String task) { + System.out.println("Processing task " + task + " for user " + userId + + " on thread " + Thread.currentThread()); + } + + // Structured concurrency for managing related tasks + public String fetchUserDataWithStructuredConcurrency(String userId) throws Exception { + try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { + + Future profile = scope.fork(() -> fetchUserProfile(userId)); + Future preferences = scope.fork(() -> fetchUserPreferences(userId)); + Future history = scope.fork(() -> fetchUserHistory(userId)); + + scope.join(); // Wait for all tasks + scope.throwIfFailed(); // Propagate any failures + + // All tasks completed successfully + return combineUserData(profile.resultNow(), + preferences.resultNow(), + history.resultNow()); + } + } + + private String fetchUserProfile(String userId) { + // Simulate I/O operation + try { Thread.sleep(100); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return "Profile for " + userId; + } + + private String fetchUserPreferences(String userId) { + try { Thread.sleep(150); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return "Preferences for " + userId; + } + + private String fetchUserHistory(String userId) { + try { Thread.sleep(200); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return "History for " + userId; + } + + private String combineUserData(String profile, String preferences, String history) { + return String.format("User data: %s, %s, %s", profile, preferences, history); + } + + public void shutdown() { + virtualExecutor.shutdown(); + } +} +``` + +**Bad example:** + +```java +// AVOID: Misusing virtual threads +import java.util.concurrent.*; + +public class BadVirtualThreadUsage { + + // BAD: Creating thread pools for virtual threads + private final ExecutorService virtualPool = Executors.newFixedThreadPool(100, + Thread.ofVirtual().factory()); // Don't pool virtual threads! + + // BAD: Using virtual threads for CPU-intensive tasks + public void performCPUIntensiveWork() { + ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor(); + + for (int i = 0; i < 1000; i++) { + virtualExecutor.submit(() -> { + // BAD: Virtual threads are not suitable for CPU-bound work + double result = 0; + for (int j = 0; j < 1_000_000; j++) { + result += Math.sqrt(j) * Math.sin(j); + } + return result; + }); + } + } + + // BAD: Using platform thread patterns with virtual threads + public void badResourceManagement() { + // Creating virtual threads manually instead of using executor + for (int i = 0; i < 10000; i++) { + Thread.ofVirtual().start(() -> { + performIOOperation(); + // BAD: No proper cleanup or error handling + }); + } + } + + // BAD: Blocking operations that shouldn't be used with virtual threads + public void problematicBlocking() { + ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor(); + + virtualExecutor.submit(() -> { + try { + synchronized (this) { // BAD: Synchronized blocks can pin virtual threads + Thread.sleep(1000); // This pins the virtual thread to platform thread + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + } + + // BAD: Using ThreadLocal instead of ScopedValue + private static final ThreadLocal USER_CONTEXT = new ThreadLocal<>(); + + public void badContextPropagation() { + USER_CONTEXT.set("user123"); + + ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor(); + virtualExecutor.submit(() -> { + // BAD: ThreadLocal values don't propagate to virtual threads properly + String userId = USER_CONTEXT.get(); // Likely null + performTaskForUser(userId); + }); + } + + private void performIOOperation() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void performTaskForUser(String userId) { + System.out.println("Processing for user: " + userId); + } +} +``` diff --git a/spml/src/test/resources/126-java-logging.mdc b/spml/src/test/resources/126-java-logging.mdc new file mode 100644 index 00000000..ac9147b0 --- /dev/null +++ b/spml/src/test/resources/126-java-logging.mdc @@ -0,0 +1,990 @@ +--- +description: Java Logging Best Practices +globs: *.java +alwaysApply: false +--- +# Java Logging Best Practices + +## System prompt characterization + +Role definition: You are a Senior software engineer with extensive experience in Java software development + +## Description + +Effective Java logging involves selecting a standard framework (SLF4J with Logback/Log4j2), using appropriate log levels (ERROR, WARN, INFO, DEBUG, TRACE), and adhering to core practices like parameterized logging, proper exception handling, and avoiding sensitive data exposure. Configuration should be environment-specific with clear output formats. Security is paramount: mask sensitive data, control log access, and ensure secure transmission. Implement centralized log aggregation, monitoring, and alerting for proactive issue detection. Finally, logging behavior and its impact should be validated through comprehensive testing. + +## Table of contents + +- Rule 1: Choose an Appropriate Logging Framework +- Rule 2: Understand and Use Logging Levels Correctly +- Rule 3: Adhere to Core Logging Practices +- Rule 4: Implement Secure Logging Practices + +## Rule 1: Choose an Appropriate Logging Framework + +Title: Select a Standard Logging Facade and Implementation +Description: Using a standard logging facade like SLF4J allows for flexibility in choosing and switching an underlying logging implementation. The primary recommendation is SLF4J with Logback for its robustness and feature-richness. + +**Good example:** + +```java +// GOOD: Using SLF4J facade with proper logger declaration +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.Objects; + +public class UserService { + // Logger declared using SLF4J - static final per class + private static final Logger logger = LoggerFactory.getLogger(UserService.class); + + public void performAction(String input) { + // SLF4J parameterized logging - efficient and readable + logger.info("Performing action with input: {}", input); + + if (Objects.isNull(input) || input.isEmpty()) { + logger.warn("Input is null or empty, using default behavior"); + input = "default"; + } + + try { + processInput(input); + logger.debug("Action completed successfully for input: {}", input); + } catch (ProcessingException e) { + logger.error("Failed to process input: {}", input, e); + throw e; + } + } + + public void performComplexOperation(String userId, String operation) { + // Using MDC (Mapped Diagnostic Context) for contextual logging + try (var mdcCloseable = org.slf4j.MDC.putCloseable("userId", userId)) { + logger.info("Starting operation: {}", operation); + + // All log statements in this block will include userId context + performInternalSteps(operation); + + logger.info("Operation completed successfully"); + } catch (Exception e) { + logger.error("Operation failed", e); + throw e; + } + } + + private void processInput(String input) throws ProcessingException { + // Simulate processing that might fail + if ("error".equals(input)) { + throw new ProcessingException("Invalid input: " + input); + } + logger.trace("Processing step completed for: {}", input); + } + + private void performInternalSteps(String operation) { + logger.debug("Executing internal step 1 for operation: {}", operation); + // ... business logic + logger.debug("Executing internal step 2 for operation: {}", operation); + // ... more business logic + } + + private static class ProcessingException extends Exception { + public ProcessingException(String message) { + super(message); + } + } +} +``` + +**Bad example:** + +```java +// AVOID: Using System.out.println or direct logging implementation +public class BadLoggingService { + + public void performAction(String input) { + // BAD: Using System.out.println - no control, no levels, no formatting + System.out.println("Starting action with: " + input); + + if (input == null || input.isEmpty()) { + // BAD: Using System.err - not integrated with logging framework + System.err.println("Warning: Input is null or empty!"); + } + + try { + processInput(input); + System.out.println("Action completed for: " + input); + } catch (Exception e) { + // BAD: Just printing stack trace to stderr + e.printStackTrace(); + // No structured logging, no log levels, no context + } + } + + // BAD: Directly using concrete logging implementation + private java.util.logging.Logger julLogger = + java.util.logging.Logger.getLogger(BadLoggingService.class.getName()); + + public void anotherMethod() { + // BAD: Tied to specific implementation, less flexible + julLogger.info("Using JUL directly"); + } + + // BAD: Multiple logging frameworks in same class + private org.apache.logging.log4j.Logger log4jLogger = + org.apache.logging.log4j.LogManager.getLogger(BadLoggingService.class); + + public void confusedLogging() { + julLogger.info("Using JUL"); + log4jLogger.info("Using Log4j"); + System.out.println("Using System.out"); + // Inconsistent and confusing! + } +} +``` + +## Rule 2: Understand and Use Logging Levels Correctly + +Title: Apply Appropriate Logging Levels for Messages +Description: Use logging levels consistently to categorize the severity and importance of log messages. ERROR for critical issues, WARN for potentially harmful situations, INFO for important business events, DEBUG for detailed information, and TRACE for fine-grained debugging. + +**Good example:** + +```java +// GOOD: Proper usage of logging levels +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.Objects; + +public class OrderProcessor { + private static final Logger logger = LoggerFactory.getLogger(OrderProcessor.class); + + public void processOrder(String orderId, String customerId) { + logger.trace("Entering processOrder with orderId: {}, customerId: {}", orderId, customerId); + + // INFO: Important business events + logger.info("Processing order {} for customer {}", orderId, customerId); + + try { + // Validate inputs + if (Objects.isNull(orderId) || orderId.trim().isEmpty()) { + // WARN: Potentially harmful situation that can be recovered + logger.warn("Order ID is null or empty for customer {}. Using generated ID.", customerId); + orderId = generateOrderId(); + } + + // DEBUG: Detailed information for development/troubleshooting + logger.debug("Validating order {} inventory", orderId); + validateInventory(orderId); + + logger.debug("Calculating pricing for order {}", orderId); + calculatePricing(orderId); + + logger.debug("Processing payment for order {}", orderId); + processPayment(orderId); + + // INFO: Successful completion of important business operation + logger.info("Order {} processed successfully for customer {}", orderId, customerId); + + } catch (InventoryException e) { + // WARN: Expected business exception that can be handled + logger.warn("Insufficient inventory for order {}. Will backorder.", orderId, e); + handleBackorder(orderId); + + } catch (PaymentException e) { + // ERROR: Critical failure requiring immediate attention + logger.error("Payment processing failed for order {}. Customer: {}", + orderId, customerId, e); + throw new OrderProcessingException("Payment failed", e); + + } catch (Exception e) { + // ERROR: Unexpected critical error + logger.error("Unexpected error processing order {} for customer {}", + orderId, customerId, e); + throw new OrderProcessingException("Unexpected error", e); + } + + logger.trace("Exiting processOrder for orderId: {}", orderId); + } + + public void performHealthCheck() { + logger.debug("Starting health check"); + + try { + checkDatabaseConnection(); + checkExternalServices(); + + // INFO: System status information + logger.info("Health check passed - all systems operational"); + + } catch (HealthCheckException e) { + // ERROR: System health issue requiring immediate attention + logger.error("Health check failed - system may be degraded", e); + } + } + + // Example methods + private String generateOrderId() { return "ORD-" + System.currentTimeMillis(); } + private void validateInventory(String orderId) throws InventoryException { /* ... */ } + private void calculatePricing(String orderId) { /* ... */ } + private void processPayment(String orderId) throws PaymentException { /* ... */ } + private void handleBackorder(String orderId) { /* ... */ } + private void checkDatabaseConnection() throws HealthCheckException { /* ... */ } + private void checkExternalServices() throws HealthCheckException { /* ... */ } + + // Exception classes + private static class InventoryException extends Exception { + public InventoryException(String message) { super(message); } + } + private static class PaymentException extends Exception { + public PaymentException(String message) { super(message); } + } + private static class OrderProcessingException extends RuntimeException { + public OrderProcessingException(String message, Throwable cause) { super(message, cause); } + } + private static class HealthCheckException extends Exception { + public HealthCheckException(String message) { super(message); } + } +} +``` + +**Bad example:** + +```java +// AVOID: Misusing logging levels +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BadLevelUsage { + private static final Logger logger = LoggerFactory.getLogger(BadLevelUsage.class); + + public void processUser(String userId) { + // BAD: Using INFO for debug-level details + logger.info("Method entry: processUser with parameter: " + userId); + logger.info("Creating new StringBuilder object"); + logger.info("Checking if userId is null"); + + if (userId == null) { + // BAD: Using ERROR for a normal business condition + logger.error("User ID is null!"); // This might be expected/handled + return; + } + + // BAD: Using WARN for normal flow information + logger.warn("User ID is not null, proceeding with processing"); + + try { + String result = processUserData(userId); + + // BAD: Using ERROR for successful operations + logger.error("Successfully processed user: " + userId + " with result: " + result); + + } catch (Exception e) { + // BAD: Using INFO for critical errors + logger.info("An error occurred: " + e.getMessage()); + // Should be ERROR or WARN depending on severity + } + + // BAD: Overuse of TRACE for everything + logger.trace("About to return from method"); + logger.trace("Setting return value to void"); + logger.trace("Method execution completed"); + // Too much noise - not useful + } + + public void anotherBadExample(String data) { + // BAD: String concatenation instead of parameterized logging + logger.info("Processing data: " + data + " at time: " + System.currentTimeMillis()); + + // BAD: Using DEBUG for important business events + logger.debug("Payment of $1000 processed for customer ABC123"); + // This should be INFO - it's an important business event + + // BAD: Using same level for different severity issues + logger.warn("Configuration file not found, using defaults"); // This is OK for WARN + logger.warn("Database connection lost"); // This should be ERROR + logger.warn("User entered invalid email format"); // This might be INFO or DEBUG + } + + private String processUserData(String userId) { + return "processed-" + userId; + } +} +``` + +## Rule 3: Adhere to Core Logging Practices + +Title: Implement Fundamental Best Practices +Description: Follow core practices including using parameterized logging, proper exception handling, avoiding sensitive data exposure, and implementing performance considerations for logging operations. + +**Good example:** + +```java +// GOOD: Core logging best practices +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import java.util.Objects; + +public class SecureTransactionService { + private static final Logger logger = LoggerFactory.getLogger(SecureTransactionService.class); + + public void processPayment(String userId, String amount, String cardNumber) { + // Set correlation ID for request tracing + String correlationId = generateCorrelationId(); + MDC.put("correlationId", correlationId); + + try { + logger.info("Processing payment for user: {}, amount: {}", userId, amount); + + // GOOD: Mask sensitive information before logging + String maskedCard = maskCreditCard(cardNumber); + logger.debug("Processing payment with card: {}", maskedCard); + + validatePaymentRequest(userId, amount, cardNumber); + + if (logger.isDebugEnabled()) { + // GOOD: Guard clause for expensive operations + String debugInfo = buildComplexDebugInfo(userId, amount); + logger.debug("Payment validation details: {}", debugInfo); + } + + processPaymentInternal(userId, amount, cardNumber); + + logger.info("Payment processed successfully for user: {}, correlation: {}", + userId, correlationId); + + } catch (ValidationException e) { + // GOOD: Log exception with context but don't expose sensitive data + logger.warn("Payment validation failed for user: {}, reason: {}, correlation: {}", + userId, e.getValidationError(), correlationId, e); + throw e; + + } catch (PaymentProcessingException e) { + // GOOD: Log critical error with full context + logger.error("Payment processing failed for user: {}, amount: {}, correlation: {}", + userId, amount, correlationId, e); + throw e; + + } catch (Exception e) { + // GOOD: Catch unexpected exceptions and log with context + logger.error("Unexpected error during payment processing for user: {}, correlation: {}", + userId, correlationId, e); + throw new PaymentProcessingException("Unexpected error", e); + + } finally { + // GOOD: Clean up MDC to prevent memory leaks + MDC.remove("correlationId"); + } + } + + public void processLargeDataSet(List dataItems) { + logger.info("Processing {} data items", dataItems.size()); + + for (int i = 0; i < dataItems.size(); i++) { + String item = dataItems.get(i); + + try { + processDataItem(item); + + // GOOD: Log progress periodically, not for every item + if (i % 1000 == 0) { + logger.debug("Processed {} of {} items", i, dataItems.size()); + } + + } catch (Exception e) { + // GOOD: Log error but continue processing + logger.warn("Failed to process item at index {}: {}", i, item, e); + } + } + + logger.info("Completed processing {} data items", dataItems.size()); + } + + // Utility methods + private String maskCreditCard(String cardNumber) { + if (cardNumber == null || cardNumber.length() < 8) { + return "****"; + } + return cardNumber.substring(0, 4) + "****" + cardNumber.substring(cardNumber.length() - 4); + } + + private String generateCorrelationId() { + return "TXN-" + System.currentTimeMillis() + "-" + Thread.currentThread().getId(); + } + + private String buildComplexDebugInfo(String userId, String amount) { + // Simulate expensive debug information building + return String.format("User: %s, Amount: %s, Timestamp: %d", + userId, amount, System.currentTimeMillis()); + } + + private void validatePaymentRequest(String userId, String amount, String cardNumber) + throws ValidationException { + if (Objects.isNull(userId) || userId.trim().isEmpty()) { + throw new ValidationException("INVALID_USER_ID"); + } + // More validation... + } + + private void processPaymentInternal(String userId, String amount, String cardNumber) + throws PaymentProcessingException { + // Simulate payment processing + if ("fail".equals(userId)) { + throw new PaymentProcessingException("Payment gateway error"); + } + } + + private void processDataItem(String item) { + // Simulate data processing + if ("error".equals(item)) { + throw new RuntimeException("Processing failed for item: " + item); + } + } + + // Exception classes + private static class ValidationException extends Exception { + private final String validationError; + + public ValidationException(String validationError) { + super("Validation failed: " + validationError); + this.validationError = validationError; + } + + public String getValidationError() { + return validationError; + } + } + + private static class PaymentProcessingException extends RuntimeException { + public PaymentProcessingException(String message) { + super(message); + } + + public PaymentProcessingException(String message, Throwable cause) { + super(message, cause); + } + } +} +``` + +**Bad example:** + +```java +// AVOID: Poor logging practices +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PoorLoggingPractices { + private static final Logger logger = LoggerFactory.getLogger(PoorLoggingPractices.class); + + public void processPayment(String userId, String amount, String cardNumber, String ssn) { + // BAD: String concatenation instead of parameterized logging + logger.info("Processing payment for user: " + userId + " amount: " + amount); + + // BAD: Logging sensitive information directly + logger.debug("Credit card: " + cardNumber + ", SSN: " + ssn); + + try { + validatePayment(userId, amount); + + // BAD: Expensive operation without guard clause + logger.debug("Payment details: " + buildExpensiveDebugString(userId, amount, cardNumber)); + + processPaymentTransaction(userId, amount, cardNumber); + + } catch (Exception e) { + // BAD: Swallowing exception without proper logging + logger.info("Payment failed: " + e.getMessage()); + // Lost the stack trace and context! + + // BAD: Logging sensitive data in error message + logger.error("Payment failed for card: " + cardNumber + " and SSN: " + ssn); + } + } + + public void processLargeDataSet(List items) { + // BAD: Logging every single item in large dataset + for (String item : items) { + logger.debug("Processing item: " + item); // Will flood logs + processItem(item); + logger.debug("Completed item: " + item); // Even more noise + } + } + + public void handleUserLogin(String username, String password) { + // BAD: Logging passwords - NEVER do this! + logger.debug("User login attempt: username=" + username + ", password=" + password); + + try { + authenticateUser(username, password); + // BAD: Inconsistent logging format + logger.info("User " + username + " logged in successfully"); + } catch (AuthenticationException e) { + // BAD: Using wrong log level and exposing sensitive info + logger.error("Login failed for " + username + " with password " + password); + } + } + + public void performDatabaseOperation(String query, String connectionString) { + // BAD: Logging database connection strings (may contain credentials) + logger.debug("Executing query: " + query + " on connection: " + connectionString); + + try { + executeQuery(query); + } catch (SQLException e) { + // BAD: Not using parameterized logging with exception + logger.error("SQL error: " + e.getMessage() + " for query: " + query); + // Stack trace is lost! + } + } + + // BAD: No try-with-resources or proper cleanup for MDC + public void badMDCUsage(String userId) { + org.slf4j.MDC.put("userId", userId); + logger.info("Processing for user"); + + // ... some processing ... + + // BAD: Forgot to clear MDC - memory leak! + // MDC.clear() or MDC.remove("userId") is missing + } + + // Helper methods with poor exception handling + private String buildExpensiveDebugString(String userId, String amount, String cardNumber) { + // Simulate expensive operation + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10000; i++) { + sb.append("Debug info for ").append(userId).append(" "); + } + return sb.toString(); + } + + private void validatePayment(String userId, String amount) throws ValidationException { + if (userId == null) throw new ValidationException("Invalid user"); + } + + private void processPaymentTransaction(String userId, String amount, String cardNumber) { + // Simulate processing + } + + private void processItem(String item) { + // Simulate processing + } + + private void authenticateUser(String username, String password) throws AuthenticationException { + if ("baduser".equals(username)) { + throw new AuthenticationException("Invalid credentials"); + } + } + + private void executeQuery(String query) throws SQLException { + if (query.contains("DROP")) { + throw new SQLException("Invalid query"); + } + } + + // Exception classes + private static class ValidationException extends Exception { + public ValidationException(String message) { super(message); } + } + private static class AuthenticationException extends Exception { + public AuthenticationException(String message) { super(message); } + } + private static class SQLException extends Exception { + public SQLException(String message) { super(message); } + } +} +``` + +## Rule 4: Implement Secure Logging Practices + +Title: Ensure Logs Do Not Compromise Security +Description: Actively mask or filter sensitive data, control access to log files, use secure transmission protocols, and comply with data protection regulations when logging information. + +**Good example:** + +```java +// GOOD: Secure logging practices +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.regex.Pattern; +import java.util.Objects; + +public class SecureLoggingService { + private static final Logger logger = LoggerFactory.getLogger(SecureLoggingService.class); + + // Patterns for detecting sensitive data + private static final Pattern CREDIT_CARD_PATTERN = Pattern.compile("\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}"); + private static final Pattern SSN_PATTERN = Pattern.compile("\\d{3}-\\d{2}-\\d{4}"); + private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"); + + public void processUserRegistration(String username, String email, String ssn, String creditCard) { + // GOOD: Sanitize all user inputs before logging + String sanitizedUsername = sanitizeForLogging(username); + String maskedEmail = maskEmail(email); + String hashedSSN = hashSensitiveData(ssn); + String maskedCard = maskCreditCard(creditCard); + + logger.info("Processing registration for user: {}, email: {}", sanitizedUsername, maskedEmail); + logger.debug("User data validation - SSN hash: {}, card mask: {}", hashedSSN, maskedCard); + + try { + validateUserData(username, email, ssn, creditCard); + createUserAccount(username, email); + + // GOOD: Log successful operation without sensitive data + logger.info("User registration completed successfully for: {}", sanitizedUsername); + + // GOOD: Security event logging for audit trail + logger.info("SECURITY_EVENT: New user registration - username: {}, timestamp: {}", + sanitizedUsername, System.currentTimeMillis()); + + } catch (ValidationException e) { + // GOOD: Log validation failure without exposing sensitive validation details + logger.warn("User registration failed validation for user: {}, error code: {}", + sanitizedUsername, e.getErrorCode()); + + } catch (SecurityException e) { + // GOOD: Security incidents logged with careful information control + logger.error("SECURITY_ALERT: Registration security violation for user: {}, violation type: {}", + sanitizeForLogging(username), e.getViolationType()); + // Don't log the actual violation details that might help attackers + + } catch (Exception e) { + // GOOD: Generic error logging without sensitive context + logger.error("User registration failed for user: {} due to system error", + sanitizedUsername, e); + } + } + + public void processPayment(String userId, PaymentRequest request) { + String correlationId = generateSecureCorrelationId(); + + try { + // GOOD: Log business operation with masked sensitive data + logger.info("Processing payment - user: {}, amount: {}, correlation: {}", + userId, request.getAmount(), correlationId); + + // GOOD: Validate and log without exposing card details + validatePaymentMethod(request.getPaymentMethod()); + logger.debug("Payment method validated for correlation: {}", correlationId); + + ChargeResult result = processCharge(request); + + // GOOD: Log success with minimal necessary information + logger.info("Payment processed successfully - correlation: {}, transaction: {}", + correlationId, result.getTransactionId()); + + // GOOD: Business intelligence logging (aggregated, non-sensitive) + logger.info("METRICS: Payment processed - amount: {}, merchant: {}, timestamp: {}", + request.getAmount(), request.getMerchantId(), System.currentTimeMillis()); + + } catch (FraudDetectedException e) { + // GOOD: Security event with controlled information + logger.error("SECURITY_ALERT: Fraud detected - correlation: {}, risk_score: {}, user: {}", + correlationId, e.getRiskScore(), userId); + // Don't log fraud detection details that could help bypass detection + + } catch (PaymentException e) { + // GOOD: Business error with safe context + logger.error("Payment failed - correlation: {}, error_type: {}", + correlationId, e.getErrorType(), e); + } + } + + // Secure data masking utilities + private String sanitizeForLogging(String input) { + if (input == null) return "[null]"; + + // Remove potentially dangerous characters for log injection protection + String sanitized = input.replaceAll("[\r\n\t]", "_") + .replaceAll("[<>\"'&]", "*"); + + // Limit length to prevent log flooding + if (sanitized.length() > 100) { + sanitized = sanitized.substring(0, 97) + "..."; + } + + return sanitized; + } + + private String maskEmail(String email) { + if (email == null || !EMAIL_PATTERN.matcher(email).matches()) { + return "[INVALID_EMAIL]"; + } + + int atIndex = email.indexOf('@'); + if (atIndex < 2) { + return "**@" + email.substring(atIndex + 1); + } + + return email.substring(0, 2) + "***@" + email.substring(atIndex + 1); + } + + private String maskCreditCard(String cardNumber) { + if (cardNumber == null || cardNumber.length() < 8) { + return "[INVALID_CARD]"; + } + + String digitsOnly = cardNumber.replaceAll("[^\\d]", ""); + if (digitsOnly.length() < 8) { + return "[INVALID_CARD]"; + } + + return digitsOnly.substring(0, 4) + "****" + + digitsOnly.substring(digitsOnly.length() - 4); + } + + private String hashSensitiveData(String data) { + if (data == null) return "[null]"; + + // Use a secure hash for audit purposes (not for security) + return "HASH_" + Math.abs(data.hashCode()); + } + + private String generateSecureCorrelationId() { + return "CORR_" + System.currentTimeMillis() + "_" + + Thread.currentThread().getId(); + } + + // Mock classes and methods + private void validateUserData(String username, String email, String ssn, String creditCard) + throws ValidationException { + if (username == null || username.trim().isEmpty()) { + throw new ValidationException("INVALID_USERNAME"); + } + } + + private void createUserAccount(String username, String email) { + // Account creation logic + } + + private void validatePaymentMethod(String paymentMethod) { + // Payment validation logic + } + + private ChargeResult processCharge(PaymentRequest request) throws PaymentException { + // Payment processing logic + return new ChargeResult("TXN_" + System.currentTimeMillis()); + } + + // Mock classes + private static class PaymentRequest { + private String amount = "100.00"; + private String paymentMethod = "card"; + private String merchantId = "MERCHANT_123"; + + public String getAmount() { return amount; } + public String getPaymentMethod() { return paymentMethod; } + public String getMerchantId() { return merchantId; } + } + + private static class ChargeResult { + private final String transactionId; + public ChargeResult(String transactionId) { this.transactionId = transactionId; } + public String getTransactionId() { return transactionId; } + } + + private static class ValidationException extends Exception { + private final String errorCode; + public ValidationException(String errorCode) { + super("Validation failed: " + errorCode); + this.errorCode = errorCode; + } + public String getErrorCode() { return errorCode; } + } + + private static class SecurityException extends Exception { + private final String violationType; + public SecurityException(String violationType) { + super("Security violation: " + violationType); + this.violationType = violationType; + } + public String getViolationType() { return violationType; } + } + + private static class FraudDetectedException extends Exception { + private final int riskScore; + public FraudDetectedException(int riskScore) { + super("Fraud detected with risk score: " + riskScore); + this.riskScore = riskScore; + } + public int getRiskScore() { return riskScore; } + } + + private static class PaymentException extends Exception { + private final String errorType; + public PaymentException(String errorType) { + super("Payment error: " + errorType); + this.errorType = errorType; + } + public String getErrorType() { return errorType; } + } +} +``` + +**Bad example:** + +```java +// AVOID: Insecure logging practices +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class InsecureLoggingService { + private static final Logger logger = LoggerFactory.getLogger(InsecureLoggingService.class); + + public void processUserRegistration(String username, String password, String email, + String ssn, String creditCard) { + // BAD: Logging passwords - NEVER do this! + logger.debug("User registration: username={}, password={}, email={}", + username, password, email); + + // BAD: Logging full SSN and credit card numbers + logger.info("Processing registration for SSN: {} with credit card: {}", ssn, creditCard); + + try { + validateUser(username, password, email, ssn, creditCard); + createAccount(username, password); + + } catch (ValidationException e) { + // BAD: Exposing sensitive validation details + logger.error("Validation failed for user {} with password {} - details: {}", + username, password, e.getValidationDetails()); + + } catch (SecurityException e) { + // BAD: Logging detailed security information that could help attackers + logger.error("Security violation for user {}: attack vector: {}, payload: {}, " + + "system_path: {}, internal_error: {}", + username, e.getAttackVector(), e.getPayload(), + e.getSystemPath(), e.getInternalError()); + } + } + + public void processPayment(String userId, String cardNumber, String cvv, String amount) { + // BAD: Logging complete payment card information + logger.info("Processing payment: user={}, card={}, cvv={}, amount={}", + userId, cardNumber, cvv, amount); + + try { + validateCard(cardNumber, cvv); + + // BAD: Logging sensitive database connection info + logger.debug("Connecting to payment DB with connection string: {}", + getDatabaseConnectionString()); + + processTransaction(cardNumber, cvv, amount); + + } catch (FraudException e) { + // BAD: Exposing fraud detection algorithms and thresholds + logger.error("Fraud detected for card {}: algorithm={}, threshold={}, " + + "risk_factors={}, detection_rules={}", + cardNumber, e.getAlgorithm(), e.getThreshold(), + e.getRiskFactors(), e.getDetectionRules()); + + } catch (PaymentException e) { + // BAD: Including full stack trace with sensitive system information + logger.error("Payment failed for card {} with full system details", cardNumber, e); + } + } + + public void handleSystemError(String operation, Exception e) { + // BAD: Logging system internals that could help attackers + logger.error("System error in operation {}: java_version={}, system_properties={}, " + + "environment_variables={}, file_paths={}", + operation, System.getProperty("java.version"), + System.getProperties(), System.getenv(), + System.getProperty("user.dir")); + + // BAD: Full stack trace might expose sensitive system information + e.printStackTrace(); // Goes to stderr, not controlled by logging config + } + + public void logUserActivity(String userId, String sessionId, String ipAddress, + String userAgent, String requestData) { + // BAD: Logging PII and potentially sensitive request data + logger.info("User activity: user={}, session={}, ip={}, userAgent={}, request={}", + userId, sessionId, ipAddress, userAgent, requestData); + + // BAD: No data retention consideration + // This could violate GDPR, CCPA, or other privacy regulations + } + + public void authenticateUser(String username, String password, String loginToken) { + // BAD: Logging authentication credentials + logger.debug("Authentication attempt: username={}, password={}, token={}", + username, password, loginToken); + + try { + boolean success = authenticate(username, password, loginToken); + if (!success) { + // BAD: Detailed failure information that could help brute force attacks + logger.warn("Login failed for {} - password_attempts={}, last_success={}, " + + "account_locked={}, failed_reasons={}", + username, getPasswordAttempts(username), + getLastSuccessfulLogin(username), isAccountLocked(username), + getFailureReasons(username)); + } + } catch (Exception e) { + // BAD: Logging authentication errors with sensitive context + logger.error("Authentication system error for user {} with credentials: " + + "password={}, token={}", username, password, loginToken, e); + } + } + + // Mock methods with security issues + private String getDatabaseConnectionString() { + return "jdbc:mysql://prod-db:3306/payments?user=admin&password=secret123"; + } + + private void validateUser(String username, String password, String email, String ssn, String creditCard) + throws ValidationException { /* ... */ } + private void createAccount(String username, String password) { /* ... */ } + private void validateCard(String cardNumber, String cvv) { /* ... */ } + private void processTransaction(String cardNumber, String cvv, String amount) + throws PaymentException { /* ... */ } + private boolean authenticate(String username, String password, String token) { return true; } + private int getPasswordAttempts(String username) { return 3; } + private long getLastSuccessfulLogin(String username) { return System.currentTimeMillis(); } + private boolean isAccountLocked(String username) { return false; } + private String getFailureReasons(String username) { return "invalid_password"; } + + // Exception classes with sensitive information exposure + private static class ValidationException extends Exception { + private final String validationDetails; + public ValidationException(String details) { + this.validationDetails = details; + } + public String getValidationDetails() { return validationDetails; } + } + + private static class SecurityException extends Exception { + private final String attackVector, payload, systemPath, internalError; + public SecurityException(String attackVector, String payload, String systemPath, String internalError) { + this.attackVector = attackVector; + this.payload = payload; + this.systemPath = systemPath; + this.internalError = internalError; + } + public String getAttackVector() { return attackVector; } + public String getPayload() { return payload; } + public String getSystemPath() { return systemPath; } + public String getInternalError() { return internalError; } + } + + private static class FraudException extends Exception { + private final String algorithm, threshold, riskFactors, detectionRules; + public FraudException(String algorithm, String threshold, String riskFactors, String detectionRules) { + this.algorithm = algorithm; + this.threshold = threshold; + this.riskFactors = riskFactors; + this.detectionRules = detectionRules; + } + public String getAlgorithm() { return algorithm; } + public String getThreshold() { return threshold; } + public String getRiskFactors() { return riskFactors; } + public String getDetectionRules() { return detectionRules; } + } + + private static class PaymentException extends Exception { + public PaymentException(String message) { super(message); } + } +} +``` diff --git a/spml/src/test/resources/131-java-unit-testing.mdc b/spml/src/test/resources/131-java-unit-testing.mdc new file mode 100644 index 00000000..4f742a28 --- /dev/null +++ b/spml/src/test/resources/131-java-unit-testing.mdc @@ -0,0 +1,1072 @@ +--- +description: +globs: +alwaysApply: false +--- +# Java Unit testing guidelines + +## System prompt characterization + +Role definition: You are a Senior software engineer with extensive experience in Java software development + +## Description + +Effective Java unit testing involves using JUnit 5 annotations and AssertJ for fluent assertions. Tests should follow the Given-When-Then structure with descriptive names for clarity. Each test must have a single responsibility, be independent, and leverage parameterized tests for data variations. Mocking dependencies with frameworks like Mockito is crucial for isolating the unit under test. While code coverage is a useful guide, the focus should be on meaningful tests for critical logic and edge cases. Test classes and methods should typically be package-private. Strategies for code splitting include small test methods and helper functions. Anti-patterns like testing implementation details, hard-coded values, and ignoring failures should be avoided. Proper state management involves isolated state and immutable objects, and error handling should include testing for expected exceptions and their messages. + +## Table of contents + +- Rule 1: Use JUnit 5 Annotations +- Rule 2: Use AssertJ for Assertions +- Rule 3: Structure Tests with Given-When-Then +- Rule 4: Use Descriptive Test Names +- Rule 5: Aim for Single Responsibility in Tests +- Rule 6: Ensure Tests are Independent +- Rule 7: Use Parameterized Tests for Data Variations +- Rule 8: Utilize Mocking for Dependencies (Mockito) +- Rule 9: Consider Test Coverage, But Don't Obsess +- Rule 10: Test Scopes +- Rule 11: Code Splitting Strategies +- Rule 12: Anti-patterns and Code Smells +- Rule 13: State Management +- Rule 14: Error Handling +- Rule 15: Leverage JSpecify for Null Safety +- Rule 16: Key Questions to Guide Test Creation (RIGHT-BICEP) +- Rule 17: Characteristics of Good Tests (A-TRIP) +- Rule 18: Verifying CORRECT Boundary Conditions + +## Rule 1: Use JUnit 5 Annotations + +Title: Prefer JUnit 5 annotations over JUnit 4 +Description: Utilize annotations from the `org.junit.jupiter.api` package (e.g., `@Test`, `@BeforeEach`, `@AfterEach`, `@DisplayName`, `@Nested`, `@Disabled`) instead of their JUnit 4 counterparts (`@org.junit.Test`, `@Before`, `@After`, `@Ignore`). This ensures consistency and allows leveraging the full capabilities of JUnit 5. + +**Good example:** + +```java +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("My Service Test") +class MyServiceTest { + + private MyService service; + + @BeforeEach + void setUp() { + service = new MyService(); // Setup executed before each test + } + + @Test + @DisplayName("should process data correctly") + void processData() { + // Given + String input = "test"; + + // When + String result = service.process(input); + + // Then + assertThat(result).isEqualTo("PROCESSED:test"); + } +} +``` + +**Bad example:** + +```java +import org.junit.Before; // JUnit 4 +import org.junit.Test; // JUnit 4 +import static org.junit.Assert.assertEquals; // JUnit 4 Assert + +public class MyServiceTest { + + private MyService service; + + @Before // JUnit 4 + public void setup() { + service = new MyService(); + } + + @Test // JUnit 4 + public void processData() { + String input = "test"; + String result = service.process(input); + assertEquals("PROCESSED:test", result); // JUnit 4 Assert + } +} +``` + +## Rule 2: Use AssertJ for Assertions + +Title: Prefer AssertJ for assertions +Description: Employ AssertJ's fluent API (`org.assertj.core.api.Assertions.assertThat`) for more readable, expressive, and maintainable assertions compared to JUnit Jupiter's `Assertions` class or Hamcrest matchers. + +**Good example:** + +```java +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class AssertJExampleTest { + + @Test + void checkValue() { + String result = "hello"; + assertThat(result) + .isEqualTo("hello") + .startsWith("hell") + .endsWith("o") + .hasSize(5); // Chain multiple assertions fluently + } + + @Test + void checkException() { + MyService service = new MyService(); + assertThatThrownBy(() -> service.divide(1, 0)) // Preferred way to test exceptions + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("zero"); + } +} +``` + +**Bad example:** + +```java +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; // JUnit Jupiter Assertions + +class JUnitAssertionsExampleTest { + + @Test + void checkValue() { + String result = "hello"; + assertEquals("hello", result); // Less fluent + assertTrue(result.startsWith("hell")); // Separate assertions for each property + assertTrue(result.endsWith("o")); + assertEquals(5, result.length()); + } + + @Test + void checkException() { + MyService service = new MyService(); + // More verbose exception testing + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> service.divide(1, 0) + ); + assertTrue(exception.getMessage().contains("zero")); // Separate assertion for message + } +} +``` + +## Rule 3: Structure Tests with Given-When-Then + +Title: Structure test methods using the Given-When-Then pattern +Description: Organize the logic within test methods into three distinct, clearly separated phases: **Given** (setup preconditions), **When** (execute the code under test), and **Then** (verify the outcome). Use comments or empty lines to visually separate these phases, enhancing readability and understanding of the test's purpose. + +**Good example:** + +```java +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + +class GivenWhenThenTest { + + @Test + void shouldCalculateSumCorrectly() { + // Given + Calculator calculator = new Calculator(); + int num1 = 5; + int num2 = 10; + int expectedSum = 15; + + // When + int actualSum = calculator.add(num1, num2); + + // Then + assertThat(actualSum).isEqualTo(expectedSum); + } +} +``` + +**Bad example:** + +```java +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + +class UnstructuredTest { + + @Test + void testAddition() { + // Lack of clear separation makes it harder to follow the test flow + Calculator calculator = new Calculator(); + assertThat(calculator.add(5, 10)).isEqualTo(15); // Combines action and verification + // Setup might be mixed with action or verification elsewhere + } +} +``` + +## Rule 4: Use Descriptive Test Names + +Title: Write descriptive test method names or use `@DisplayName` +Description: Test names should clearly communicate the scenario being tested and the expected outcome. Use either descriptive method names (e.g., following the `should_ExpectedBehavior_when_StateUnderTest` pattern) or JUnit 5's `@DisplayName` annotation for more natural language descriptions. This makes test reports easier to understand. + +**Good example:** + +```java +@Test +void should_throwException_when_divisorIsZero() { + // Given + Calculator calculator = new Calculator(); + + // When & Then + assertThatThrownBy(() -> calculator.divide(1, 0)) + .isInstanceOf(ArithmeticException.class); +} + +@Test +@DisplayName("Should return the correct sum for positive numbers") +void additionWithPositives() { + // Given + Calculator calculator = new Calculator(); + int num1 = 5; + int num2 = 10; + + // When + int actualSum = calculator.add(num1, num2); + + // Then + assertThat(actualSum).isEqualTo(15); +} +``` + +**Bad example:** + +```java +@Test +void testDivide() { // Name is too generic, doesn't explain the scenario + // ... test logic ... +} + +@Test +void test1() { // Uninformative name + // ... test logic ... +} +``` + +## Rule 5: Aim for Single Responsibility in Tests + +Title: Each test method should verify a single logical concept +Description: Avoid testing multiple unrelated things within a single test method. Each test should focus on one specific aspect of the unit's behavior or one particular scenario. This makes tests easier to understand, debug, and maintain. If a test fails, its specific focus makes pinpointing the cause simpler. + +**Good example:** + +```java +// Separate tests for different validation aspects +@Test +void should_reject_when_emailIsNull() { + // ... test logic for null email ... +} + +@Test +void should_reject_when_emailFormatIsInvalid() { + // ... test logic for invalid email format ... +} +``` + +**Bad example:** + +```java +@Test +void testUserValidation() { // Tests multiple conditions at once + // Given user with null email + // ... assertion for null email ... + + // Given user with invalid email format + // ... assertion for invalid format ... + + // Given user with valid email + // ... assertion for valid email ... +} +``` + +## Rule 6: Ensure Tests are Independent + +Title: Tests must be independent and runnable in any order +Description: Avoid creating tests that depend on the state left behind by previously executed tests. Each test should set up its own required preconditions (using `@BeforeEach` or within the test method itself) and should not rely on the execution order. This ensures test suite stability and reliability, preventing flickering tests. + +**Good example:** + +```java +class IndependentTests { + private MyRepository repository = new InMemoryRepository(); // Or use @BeforeEach + + @Test + void should_findItem_when_itemExists() { + // Given + Item item = new Item("testId", "TestData"); + repository.save(item); // Setup specific to this test + + // When + Optional found = repository.findById("testId"); + + // Then + assertThat(found).isPresent(); + } + + @Test + void should_returnEmpty_when_itemDoesNotExist() { + // Given - Repository is clean (or re-initialized via @BeforeEach) + + // When + Optional found = repository.findById("nonExistentId"); + + // Then + assertThat(found).isNotPresent(); + } +} +``` + +**Bad example:** + +```java +class DependentTests { + private static MyRepository repository = new InMemoryRepository(); // Shared state + private static Item savedItem; + + @Test // Test 1 (might run first) + void testSave() { + savedItem = new Item("testId", "Data"); + repository.save(savedItem); + // ... assertions ... + } + + @Test // Test 2 (depends on Test 1 having run) + void testFind() { + // This test fails if testSave() hasn't run or if run order changes + Optional found = repository.findById("testId"); + assertThat(found).isPresent(); + } +} +``` + +## Rule 7: Use Parameterized Tests for Data Variations + +Title: Use `@ParameterizedTest` for testing the same logic with different inputs +Description: When testing a method's behavior across various input values or boundary conditions, leverage JUnit 5's parameterized tests (`@ParameterizedTest` with sources like `@ValueSource`, `@CsvSource`, `@MethodSource`). This avoids code duplication and clearly separates the test logic from the test data. + +**Good example:** + +```java +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import static org.assertj.core.api.Assertions.assertThat; + +class ParameterizedCalculatorTest { + + private final Calculator calculator = new Calculator(); + + @ParameterizedTest(name = "{index} {0} + {1} = {2}") // Clear naming for each case + @CsvSource({ + "1, 2, 3", + "0, 0, 0", + "-5, 5, 0", + "10, -3, 7" + }) + void additionTest(int a, int b, int expectedResult) { + // Given inputs a, b (from @CsvSource) + + // When + int actualResult = calculator.add(a, b); + + // Then + assertThat(actualResult).isEqualTo(expectedResult); + } +} +``` + +**Bad example:** + +```java +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + +class RepetitiveCalculatorTest { + + private final Calculator calculator = new Calculator(); + + // Redundant tests for the same logic + @Test + void add1and2() { + assertThat(calculator.add(1, 2)).isEqualTo(3); + } + + @Test + void add0and0() { + assertThat(calculator.add(0, 0)).isEqualTo(0); + } + + @Test + void addNegative5and5() { + assertThat(calculator.add(-5, 5)).isEqualTo(0); + } + + @Test + void add10andNegative3() { + assertThat(calculator.add(10, -3)).isEqualTo(7); + } +} +``` + +## Rule 8: Utilize Mocking for Dependencies (Mockito) + +Title: Isolate the unit under test using mocking frameworks like Mockito +Description: Unit tests should focus solely on the logic of the class being tested (System Under Test - SUT), not its dependencies (database, network services, other classes). Use mocking frameworks like Mockito to create mock objects that simulate the behavior of these dependencies. This ensures tests are fast, reliable, and truly test the unit in isolation. + +**Key Mockito Concepts:** + +* **`mock(Class classToMock)`**: Creates a mock object of a given class or interface. +* **`when(mock.methodCall()).thenReturn(value)`**: Defines the behavior of a mock object's method. When the specified method is called on the mock, it will return the defined `value`. +* **`verify(mock).methodCall()`**: Verifies that a specific method was called on the mock object. You can also specify the number of times (`times(n)`), at least once (`atLeastOnce()`), etc. +* **`@Mock` Annotation**: Used with `@ExtendWith(MockitoExtension.class)` (JUnit 5) to automatically create mocks for fields. +* **`@InjectMocks` Annotation**: Creates an instance of the class under test and automatically injects fields annotated with `@Mock` into it. + + +**Good example:** + +```java +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; // Import static methods + +// Assume classes: UserService, UserRepository, User + +@ExtendWith(MockitoExtension.class) // Integrate Mockito with JUnit 5 +class UserServiceTest { + + @Mock // Create a mock UserRepository + private UserRepository userRepository; + + @InjectMocks // Create UserService instance and inject the mock repository + private UserService userService; + + @Test + @DisplayName("Should return user when found by id") + void findUserById_Success() { + // Given + User expectedUser = new User("123", "John Doe"); + // Define mock behavior: when findById("123") is called, return our user + when(userRepository.findById("123")).thenReturn(Optional.of(expectedUser)); + + // When + Optional actualUser = userService.findUserById("123"); + + // Then + assertThat(actualUser).isPresent().contains(expectedUser); + // Verify that findById("123") was called exactly once on the mock repository + verify(userRepository, times(1)).findById("123"); + verifyNoMoreInteractions(userRepository); // Optional: ensure no other methods were called + } + + @Test + @DisplayName("Should return empty optional when user not found") + void findUserById_NotFound() { + // Given + // Define mock behavior: when findById is called with any string, return empty + when(userRepository.findById(anyString())).thenReturn(Optional.empty()); + + // When + Optional actualUser = userService.findUserById("unknownId"); + + // Then + assertThat(actualUser).isNotPresent(); + verify(userRepository).findById("unknownId"); // Verify the specific call + } + + @Test + @DisplayName("Should save user successfully") + void saveUser() { + // Given + User userToSave = new User(null, "Jane Doe"); // ID might be generated on save + User savedUser = new User("genId", "Jane Doe"); + // Define behavior for save: return the user with an ID + when(userRepository.save(any(User.class))).thenReturn(savedUser); + + // When + User result = userService.saveUser(userToSave); + + // Then + assertThat(result).isEqualTo(savedUser); + // Verify that save was called with the correct user object (or use ArgumentCaptor for complex cases) + verify(userRepository).save(userToSave); + } +} +``` + +**Bad example:** + +```java +import org.junit.jupiter.api.Test; + +class UserServiceTestBad { + + @Test + void findUserById() { + // Bad: Using real dependencies instead of mocks + DatabaseConnection connection = new DatabaseConnection("localhost", 5432); + UserRepository userRepository = new PostgresUserRepository(connection); + UserService userService = new UserService(userRepository); + + // This test depends on database availability and state + Optional user = userService.findUserById("123"); + + // Test is slow, brittle, and doesn't isolate the unit under test + assertThat(user).isPresent(); + } +} +``` + +## Rule 9: Consider Test Coverage, But Don't Obsess + +Title: Use code coverage as a guide, not a definitive quality metric +Description: Tools like JaCoCo can measure which lines of code are executed by your tests (code coverage). Aiming for high coverage (e.g., >80% line/branch coverage) is generally good practice, as it indicates most code paths are tested. However, 100% coverage doesn't guarantee bug-free code or high-quality tests. Focus on writing meaningful tests for critical logic and edge cases rather than solely chasing coverage numbers. A test might cover a line but not actually verify its correctness effectively. + +## Rule 10: Test Scopes + +Title: Test classes and methods should be package-private +Description: Test classes should have package-private visibility. There is no need for them to be public. Test methods should have package-private visibility. There is no need for them to be public. + +## Rule 11: Code Splitting Strategies + +Title: Keep test methods focused and use proper organization +Description: Small Test Methods: Keep test methods small and focused on testing a single behavior. Helper Methods: Use helper methods to avoid code duplication in test setup and assertions. Parameterized Tests: Utilize JUnit's parameterized tests to test the same logic with different input values. + +## Rule 12: Anti-patterns and Code Smells + +Title: Avoid common testing anti-patterns +Description: Testing Implementation Details: Avoid testing implementation details that might change, leading to brittle tests. Focus on testing behavior and outcomes. Hard-coded Values: Avoid hard-coding values in tests. Use constants or test data to make tests more maintainable. Complex Test Logic: Keep test logic simple and avoid complex calculations or conditional statements within tests. Ignoring Edge Cases: Don't ignore edge cases or boundary conditions. Ensure tests cover a wide range of inputs, including invalid or unexpected values. Slow Tests: Avoid slow tests that discourage developers from running them frequently. Over-reliance on Mocks: Mock judiciously; too many mocks can obscure the actual behavior and make tests less reliable. Ignoring Test Failures: Never ignore failing tests. Investigate and fix them promptly. + +## Rule 13: State Management + +Title: Ensure proper state isolation between tests +Description: - **Isolated State:** Ensure each test has its own isolated state to avoid interference between tests. Use `@BeforeEach` to reset the state before each test. - **Immutable Objects:** Prefer immutable objects to simplify state management and avoid unexpected side effects. - **Stateless Components:** Design stateless components whenever possible to reduce the need for state management in tests. + +## Rule 14: Error Handling + +Title: Test error conditions and exception handling +Description: - **Expected Exceptions:** Use AssertJ's `assertThatThrownBy` to verify that a method throws the expected exception under specific conditions. - **Exception Messages:** Assert the exception message to ensure the correct error is being thrown with helpful context. - **Graceful Degradation:** Test how the application handles errors and gracefully degrades when dependencies are unavailable. + +## Rule 15: Leverage JSpecify for Null Safety + +Title: Utilize JSpecify annotations for explicit nullness contracts +Description: Employ JSpecify annotations (org.jspecify.annotations.*) such as @NullMarked, @Nullable, and @NonNull to clearly define the nullness expectations of method parameters, return types, and fields within your tests and the code under test. This practice enhances code clarity, enables static analysis tools to catch potential NullPointerExceptions early, and improves the overall robustness of your tests and application code. In test code, this is particularly important for defining the expected behavior of mocks and verifying interactions with potentially null values. + +**Good example:** + +```java +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +// Assume: +// @NullMarked // Usually at package-info.java or a higher-level class +// interface DataService { +// @Nullable String getData(String key); +// String processData(@Nullable String data); +// } +// +// class MyProcessor { +// private final DataService dataService; +// +// MyProcessor(DataService dataService) { +// this.dataService = dataService; +// } +// +// String execute(String key) { +// @Nullable String rawData = dataService.getData(key); +// // rawData could be null here, static analysis would warn if not checked +// if (rawData == null) { +// return dataService.processData(null); +// } +// return dataService.processData(rawData.toUpperCase()); +// } +// } + + +@NullMarked // Applies non-null by default to this test class +@ExtendWith(MockitoExtension.class) +class MyProcessorTest { + + @Mock + private DataService mockDataService; + + private MyProcessor myProcessor; + + @Test + void should_handleNullData_when_serviceReturnsNull() { + // Given + myProcessor = new MyProcessor(mockDataService); + String key = "testKey"; + // JSpecify helps clarify that getData can return null + when(mockDataService.getData(key)).thenReturn(null); + // JSpecify helps clarify that processData can accept null + when(mockDataService.processData(null)).thenReturn("processed:null"); + + + // When + String result = myProcessor.execute(key); + + // Then + assertThat(result).isEqualTo("processed:null"); + } + + @Test + void should_processNonNullData_when_serviceReturnsData() { + // Given + myProcessor = new MyProcessor(mockDataService); + String key = "testKey"; + String serviceData = "someData"; // Effectively @NonNull due to @NullMarked context + // getData's return is @Nullable, but we are testing the non-null path + when(mockDataService.getData(key)).thenReturn(serviceData); + // processData's argument is @Nullable, here we pass a non-null value + when(mockDataService.processData("SOMEDATA")).thenReturn("processed:SOMEDATA"); + + // When + String result = myProcessor.execute(key); + + // Then + assertThat(result).isEqualTo("processed:SOMEDATA"); + } +} +``` + +**Bad example:** + +```java +// No JSpecify annotations, nullness is ambiguous +// class DataService { +// String getData(String key); // Is null return possible? Is key nullable? +// String processData(String data); // Can data be null? +// } +// +// class MyProcessor { +// // ... constructor ... +// String execute(String key) { +// String rawData = dataService.getData(key); +// // Potential NPE here if getData can return null and it's not handled. +// // The contract is unclear without annotations. +// return dataService.processData(rawData.toUpperCase()); +// } +// } + +class MyProcessorTest { + // ... test setup ... + + @Test + void testProcessing() { + // Given + MyProcessor myProcessor = new MyProcessor(mockDataService); + String key = "testKey"; + // Ambiguity: if getData returns null, this test might pass or fail unexpectedly + // depending on mock setup, but the underlying contract isn't clear. + when(mockDataService.getData(key)).thenReturn("someData"); + when(mockDataService.processData("SOMEDATA")).thenReturn("processed:SOMEDATA"); + // If getData could return null and mockDataService.processData isn't + // prepared for mockDataService.processData(null), an NPE could occur + // in the code under test or the test itself, masking the real issue. + + // When + String result = myProcessor.execute(key); + + // Then + assertThat(result).isEqualTo("processed:SOMEDATA"); + } +} +``` + +## Rule 16: Key Questions to Guide Test Creation (RIGHT-BICEP) + +Title: Use RIGHT-BICEP to guide comprehensive test creation +Description: Key Questions to Guide Test Creation + +**Description:** + +* **If the code ran correctly, how would I know?**: +* **How am I going to test this?**: +* **What else can go wrong?**: +* **Could this same kind of problem happen anywhere else?**: +* **What to Test: Use Your RIGHT-BICEP**: +* **Are the results **R**ight?**: +* **Are all the **B**oundary conditions CORRECT?**: +* **Can you check **I**nverse relationships?**: +* **Can you **C**ross-check results using other means?**: +* **Can you force **E**rror conditions to happen?**: +* **Are **P**erformance characteristics within bounds?**: + + +**Good example:** + +```java +// Testing a calculator's add method, considering RIGHT-BICEP +public class Calculator { + public int add(int a, int b) { + if ((long)a + b > Integer.MAX_VALUE || (long)a + b < Integer.MIN_VALUE) { + throw new ArithmeticException("Integer overflow"); + } + return a + b; + } +} + +// Test class +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class CalculatorTest { + + private final Calculator calculator = new Calculator(); + + // R - Right results + @Test + void add_simplePositiveNumbers_returnsCorrectSum() { + assertThat(calculator.add(2, 3)).isEqualTo(5); + } + + // B - Boundary conditions (e.g., with zero, negative numbers, max/min values) + @Test + void add_numberAndZero_returnsNumber() { + assertThat(calculator.add(5, 0)).isEqualTo(5); + } + + @Test + void add_positiveAndNegative_returnsCorrectSum() { + assertThat(calculator.add(5, -4)).isEqualTo(1); + } + + @Test + void add_nearMaxInteger_returnsCorrectSum() { + assertThat(calculator.add(Integer.MAX_VALUE - 1, 1)).isEqualTo(Integer.MAX_VALUE); + } + + // I - Inverse relationships (e.g., subtraction) + // (Not directly applicable for a simple add, but if we had subtract: result - b == a) + + // C - Cross-check (e.g., add(a,b) == add(b,a)) + @Test + void add_commutativeProperty_holdsTrue() { + assertThat(calculator.add(2, 3)).isEqualTo(calculator.add(3, 2)); + } + + // E - Error conditions (e.g., overflow) + @Test + void add_integerOverflow_throwsArithmeticException() { + assertThatThrownBy(() -> calculator.add(Integer.MAX_VALUE, 1)) + .isInstanceOf(ArithmeticException.class) + .hasMessageContaining("overflow"); + } + + @Test + void add_integerUnderflow_throwsArithmeticException() { + assertThatThrownBy(() -> calculator.add(Integer.MIN_VALUE, -1)) + .isInstanceOf(ArithmeticException.class) + .hasMessageContaining("overflow"); + } + + // P - Performance (Not typically unit tested unless specific requirements) + // @Test + // void add_performance_isWithinAcceptableLimits() { + // // Potentially a more complex performance test scenario + // } +} +``` + +**Bad example:** + +```java +// Test only covers one simple case (violates "Thorough" from A-TRIP, and doesn't consider RIGHT-BICEP) +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + +public class CalculatorTestPoor { + private final Calculator calculator = new Calculator(); + + @Test + void add_basicTest() { + assertThat(calculator.add(2, 2)).isEqualTo(4); // Only testing one happy path. + // No boundary conditions, no error conditions, etc. + } +} +``` + +## Rule 17: Characteristics of Good Tests (A-TRIP) + +Title: Good tests are A-TRIP +Description: Good tests are A-TRIP: - **A**utomatic: Tests should run without human intervention. - **T**horough: Test everything that could break; cover edge cases. - **R**epeatable: Tests should produce the same results every time, in any environment. - **I**ndependent: Tests should not rely on each other or on the order of execution. - **P**rofessional: Test code is real code; keep it clean, maintainable, and well-documented. + +**Good example:** + +```java +// Demonstrating A-TRIP principles +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +// Production class (simplified) +class OrderProcessor { + private List items; + + public OrderProcessor() { + this.items = new ArrayList<>(); + } + + public void addItem(String item) { + if (Objects.isNull(item) || item.trim().isEmpty()) { + throw new IllegalArgumentException("Item cannot be null or empty"); + } + this.items.add(item); + } + + public int getItemCount() { + return this.items.size(); + } + + public void clearItems() { + this.items.clear(); + } +} + +// Test class demonstrating A-TRIP +public class OrderProcessorTest { + + private OrderProcessor processor; + + // Automatic: Part of JUnit test suite, runs with build tools. + // Independent: Each test sets up its own state. + @BeforeEach + void setUp() { + processor = new OrderProcessor(); // Fresh instance for each test + } + + // Thorough: Testing adding valid items. + @Test + void addItem_validItem_increasesCount() { + processor.addItem("Laptop"); + assertThat(processor.getItemCount()).isEqualTo(1); + processor.addItem("Mouse"); + assertThat(processor.getItemCount()).isEqualTo(2); + } + + // Thorough: Testing an edge case (adding null). + @Test + void addItem_nullItem_throwsIllegalArgumentException() { + assertThatThrownBy(() -> processor.addItem(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + // Thorough: Testing another edge case (adding empty string). + @Test + void addItem_emptyItem_throwsIllegalArgumentException() { + assertThatThrownBy(() -> processor.addItem(" ")) + .isInstanceOf(IllegalArgumentException.class); + } + + // Repeatable: No external dependencies that change (e.g., time, random numbers without seed). + // This test will always pass or always fail consistently. + @Test + void getItemCount_afterClearing_isZero() { + processor.addItem("Keyboard"); + processor.clearItems(); + assertThat(processor.getItemCount()).isEqualTo(0); + } + + // Professional: Code is clean, uses meaningful names, follows conventions. +} +``` + +**Bad example:** + +```java +// Violating A-TRIP principles +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + +public class BadOrderProcessorTest { + // Violates Independent: static processor shared between tests can lead to order dependency. + private static OrderProcessor sharedProcessor = new OrderProcessor(); + + @Test + void test1_addItem() { + // Assumes this runs first or that sharedProcessor is empty. + sharedProcessor.addItem("Book"); + assertThat(sharedProcessor.getItemCount()).isEqualTo(1); // Might fail if other tests run first and add items. + // This makes it NOT Repeatable consistently. + } + + @Test + void test2_addAnotherItem() { + sharedProcessor.addItem("Pen"); + // The expected count depends on whether test1_addItem ran and succeeded. + // assertThat(sharedProcessor.getItemCount()).isEqualTo(2); // Unreliable + assertThat(sharedProcessor.getItemCount()).isGreaterThan(0); // Weaker assertion due to lack of independence. + } + + // Violates Automatic: If a test required manual setup (e.g., "Ensure database server X is running and has Y data") + // Violates Thorough: Might only test happy paths. + // Violates Professional: Unclear test names, messy code, reliance on external state. +} +``` + +## Rule 18: Verifying CORRECT Boundary Conditions + +Title: Ensure your tests check CORRECT boundary conditions +Description: Ensure your tests check the following boundary conditions (CORRECT): - **C**onformance: Does the value conform to an expected format? (e.g., email format, date format) - **O**rdering: Is the set of values ordered or unordered as appropriate? (e.g., sorted list, items in a queue) - **R**ange: Is the value within reasonable minimum and maximum values? (e.g., age > 0, percentage 0-100) - **R**eference: Does the code reference anything external that isn't under direct control of the code itself? (e.g., file existence, network connection - often for integration tests, but can apply to unit tests with mocks) - **E**xistence: Does the value exist? (e.g., is non-null, non-zero, present in a set) - **C**ardinality: Are there exactly enough values? (e.g., expecting 1 result, 0 results, N results) - **T**ime (absolute and relative): Is everything happening in order? At the right time? In time? (e.g., timeouts, sequence of events) + +**Good example:** + +```java +// Example: Testing a method that validates a user registration age. +import java.util.Objects; +public class UserValidation { + private static final int MIN_AGE = 18; + private static final int MAX_AGE = 120; + + public boolean isAgeValid(int age) { + // R - Range + return age >= MIN_AGE && age <= MAX_AGE; + } + + public boolean isValidEmailFormat(String email) { + if (Objects.isNull(email)) return false; + // C - Conformance (simplified regex for demonstration) + return email.matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"); + } + + // E - Existence + public boolean processUsername(String username) { + if (Objects.isNull(username) || username.trim().isEmpty()) { + // Checks for existence (non-null, non-empty) + return false; + } + // process username + return true; + } +} + +// Test class +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import static org.assertj.core.api.Assertions.assertThat; + +public class UserValidationTest { + private final UserValidation validator = new UserValidation(); + + // Testing Range for age + @Test + void isAgeValid_ageAtLowerBound_returnsTrue() { + assertThat(validator.isAgeValid(18)).isTrue(); + } + + @Test + void isAgeValid_ageAtUpperBound_returnsTrue() { + assertThat(validator.isAgeValid(120)).isTrue(); + } + + @Test + void isAgeValid_ageWithinBounds_returnsTrue() { + assertThat(validator.isAgeValid(35)).isTrue(); + } + + @Test + void isAgeValid_ageBelowLowerBound_returnsFalse() { + assertThat(validator.isAgeValid(17)).isFalse(); + } + + @Test + void isAgeValid_ageAboveUpperBound_returnsFalse() { + assertThat(validator.isAgeValid(121)).isFalse(); + } + + // Testing Conformance for email + @ParameterizedTest + @ValueSource(strings = {"user@example.com", "user.name@sub.example.co.uk"}) + void isValidEmailFormat_validEmails_returnsTrue(String email) { + assertThat(validator.isValidEmailFormat(email)).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = {"userexample.com", "user@", "@example.com", "user @example.com", ""}) + void isValidEmailFormat_invalidEmails_returnsFalse(String email) { + assertThat(validator.isValidEmailFormat(email)).isFalse(); + } + + @Test + void isValidEmailFormat_nullEmail_returnsFalse() { + assertThat(validator.isValidEmailFormat(null)).isFalse(); + } + + // Testing Existence for username + @Test + void processUsername_validUsername_returnsTrue() { + assertThat(validator.processUsername("john_doe")).isTrue(); + } + + @Test + void processUsername_nullUsername_returnsFalse() { + assertThat(validator.processUsername(null)).isFalse(); + } + + @Test + void processUsername_emptyUsername_returnsFalse() { + assertThat(validator.processUsername("")).isFalse(); + } + + @Test + void processUsername_blankUsername_returnsFalse() { + assertThat(validator.processUsername(" ")).isFalse(); + } +} +``` + +**Bad example:** + +```java +// Only testing one happy path for age validation, ignoring boundaries. +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + +public class UserValidationPoorTest { + private final UserValidation validator = new UserValidation(); + + @Test + void isAgeValid_typicalAge_returnsTrue() { + assertThat(validator.isAgeValid(25)).isTrue(); // Only one value tested. + // Min, max, below min, above max are not tested. + } + + @Test + void isValidEmailFormat_typicalEmail_returnsTrue() { + assertThat(validator.isValidEmailFormat("test@example.com")).isTrue(); // No invalid formats, no nulls. + } +} +``` \ No newline at end of file diff --git a/spml/src/test/resources/141-java-refactoring-with-modern-features.mdc b/spml/src/test/resources/141-java-refactoring-with-modern-features.mdc new file mode 100644 index 00000000..da58d993 --- /dev/null +++ b/spml/src/test/resources/141-java-refactoring-with-modern-features.mdc @@ -0,0 +1,1113 @@ +--- +description: +globs: +alwaysApply: false +--- +# Modern Java Development Guidelines (Java 8+) + +## System prompt characterization + +Role definition: You are a Senior software engineer with extensive experience in Java software development + +## Description + +Modern Java development (Java 8+) emphasizes leveraging lambda expressions and functional interfaces over anonymous classes, and using the Stream API for declarative collection processing. The `Optional` API should be used for handling potentially absent values gracefully, and the `java.time` API for all date/time operations. Default methods allow non-breaking interface evolution. Local Variable Type Inference (`var`) can improve readability when used judiciously. Unmodifiable collection factory methods (`List.of()`, etc.) provide concise immutable collections. `CompletableFuture` facilitates composable asynchronous programming. The Java Platform Module System (JPMS, Java 9+) enables strong encapsulation. Performance implications of new features should be considered and profiled. Testing strategies need to adapt to these modern features, and text blocks (Java 15+) offer improved readability for multi-line strings. + +## Table of contents + +- Rule 1: Lambda Expressions and Functional Interfaces +- Rule 2: Stream API +- Rule 3: Optional API +- Rule 4: Date/Time API (java.time) +- Rule 5: Default Methods in Interfaces +- Rule 6: Local Variable Type Inference (var) +- Rule 7: Collection Factory Methods +- Rule 8: CompletableFuture for Asynchronous Programming +- Rule 9: Module System (Java 9+) +- Rule 10: Performance Considerations with Modern Features +- Rule 11: Testing Modern Java Code +- Rule 12: Use Text Blocks for Readable Multi-line Strings + +## Rule 1: Lambda Expressions and Functional Interfaces + +Title: Effectively Use Lambda Expressions and Functional Interfaces +Description: - Prefer lambda expressions over anonymous inner classes for concise implementation of functional interfaces. - Keep lambda expressions short, readable, and focused on a single piece of logic. - Use method references (e.g., `System.out::println`, `String::isEmpty`) when they are clearer and more direct than an equivalent lambda. - Leverage the rich set of built-in functional interfaces from the `java.util.function` package (e.g., `Predicate`, `Function`, `Consumer`, `Supplier`). - Create custom functional interfaces only when a specific signature is needed that isn't covered by built-in ones. - Always annotate custom functional interfaces with `@FunctionalInterface` to ensure they meet the criteria (a single abstract method) and to clearly communicate their purpose. + +**Good example:** + +```java +import java.util.List; +import java.util.stream.Collectors; +import java.util.Arrays; +import java.util.function.Predicate; + +// Custom functional interface +@FunctionalInterface +interface DataProcessor { + R process(T data); +} + +public class LambdaExample { + public static void main(String[] args) { + List names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve"); + + // Good: Using method reference + System.out.println("Printing names using method reference:"); + names.forEach(System.out::println); + + // Good: Simple lambda for filtering + List longNames = names.stream() + .filter(str -> str.length() > 4) // Lambda expression + .collect(Collectors.toList()); + System.out.println("\nLong names (length > 4): " + longNames); + + // Good: Using a built-in functional interface (Predicate) + Predicate startsWithA = s -> s.startsWith("A"); + List namesStartingWithA = names.stream() + .filter(startsWithA) + .collect(Collectors.toList()); + System.out.println("Names starting with 'A': " + namesStartingWithA); + + // Good: Using a custom functional interface + DataProcessor nameLengthProcessor = name -> name.length(); + int lengthOfAlice = nameLengthProcessor.process("Alice"); + System.out.println("Length of 'Alice' using custom processor: " + lengthOfAlice); + } +} +``` + +**Bad example:** + +```java +import java.util.List; +import java.util.Arrays; +import java.util.function.Consumer; + +public class OldStyleAnonymousClass { + public static void main(String[] args) { + List names = Arrays.asList("Alice", "Bob"); + + // Bad: Using an anonymous inner class where a lambda would be more concise + names.forEach(new Consumer() { + @Override + public void accept(String s) { + System.out.println("Processing: " + s); + } + }); + + // Bad: Overly complex lambda that should be a separate method + names.stream().filter(s -> { + System.out.println("Checking name: " + s); // Side effect in filter + boolean isLong = s.length() > 3; + boolean startsWithVowel = "AEIOUaeiou".indexOf(s.charAt(0)) != -1; + return isLong && startsWithVowel; // Multiple conditions + }).forEach(System.out::println); + } +} +``` + +## Rule 2: Stream API + +Title: Leverage the Stream API for Collection Processing +Description: Use streams for processing sequences of elements from collections or other sources in a functional style. Prefer a declarative approach (what to do) over an imperative one (how to do it) for stream operations. Chain stream operations effectively to create a readable processing pipeline. Use appropriate terminal operations to produce a result or side-effect. Be mindful of performance implications, especially with large datasets or complex operations. + +**Good example:** + +```java +import java.util.List; +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Collectors; + +public class StreamApiExample { + public static void main(String[] args) { + List data = Arrays.asList(" apple ", null, " BANANA ", " cherry ", "apple", " "); + + // Good: Effective stream usage for cleaning and processing data + List processedData = data.stream() + .filter(Objects::nonNull) // Remove nulls + .map(String::trim) // Trim whitespace + .filter(s -> !s.isEmpty()) // Remove empty strings + .map(String::toLowerCase) // Convert to lower case + .distinct() // Keep unique elements + .sorted() // Sort alphabetically + .collect(Collectors.toList()); + System.out.println("Processed and sorted data: " + processedData); + + // Good: Complex data transformation pipeline + List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + int sumOfEvenSquares = numbers.stream() + .filter(n -> n % 2 == 0) + .mapToInt(n -> n * n) + .sum(); + System.out.println("Sum of squares of even numbers: " + sumOfEvenSquares); + } +} +``` + +**Bad example:** + +```java +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; + +public class StreamAntiPattern { + public static void main(String[] args) { + List data = Arrays.asList("a", "b", "c"); + List result = new ArrayList<>(); + + // Bad: Using stream for a simple loop where a foreach loop is clearer + // and potentially more performant for simple side effects on small lists. + data.stream().forEach(s -> result.add(s.toUpperCase())); // Modifying external list - side effect! + + // Better (imperative but clear for this simple case, or use map().collect()): + for (String s : data) { + result.add(s.toUpperCase()); + } + System.out.println("Uppercase (imperative): " + result); + + // Bad: Overusing parallel streams for simple operations on small collections + // The overhead of parallelization can outweigh benefits. + List numbers = Arrays.asList(1, 2, 3, 4, 5); + int sum = numbers.parallelStream() // Unnecessary parallelization for small sum + .reduce(0, Integer::sum); + System.out.println("Sum (parallel, overkill): " + sum); + } +} +``` + +## Rule 3: Optional API + +Title: Handle Potentially Absent Values Gracefully with Optional +Description: Use `Optional` to explicitly represent values that may be absent, making your API clearer about nullability. Avoid calling `Optional.get()` directly without first checking `isPresent()`. Prefer functional alternatives. Leverage `Optional`'s functional-style methods like `map()`, `flatMap()`, `filter()`, `orElse()`, `orElseGet()`, `orElseThrow()` to handle absent values in a fluent and safe manner. + +**Good example:** + +```java +import java.util.Optional; +import java.util.Map; + +class User { + Address address; + String name; + public User(String name, Address address) { this.name = name; this.address = address; } + public Optional
getAddress() { return Optional.ofNullable(address); } + public String getName() { return name; } +} +class Address { + Country country; + public Address(Country c) { this.country = c; } + public Optional getCountry() { return Optional.ofNullable(country); } +} +class Country { + String code; + public Country(String code) { this.code = code; } + public String getCode() { return code; } +} + +public class OptionalExample { + private static Map userRepository = Map.of( + "user1", new User("Alice", new Address(new Country("US"))), + "user2", new User("Bob", new Address(null)), // User with address, but no country + "user3", new User("Charlie", null) // User with no address + ); + + public static String findUserCountryCode(String userId) { + // Good: Complex Optional chain to safely navigate potentially null properties + return Optional.ofNullable(userRepository.get(userId)) // Optional + .flatMap(User::getAddress) // Optional
+ .flatMap(Address::getCountry) // Optional + .map(Country::getCode) // Optional (country code) + .orElse("UNKNOWN_COUNTRY"); // Default if any step results in empty + } + + public static String processValue(String value) { + // Good: Optional usage for simple processing + return Optional.ofNullable(value) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(String::toUpperCase) + .orElse("DEFAULT_VALUE"); + } +} +``` + +**Bad example:** + +```java +import java.util.Optional; + +public class OptionalAntiPattern { + + public String getValueUnsafe(Optional optValue) { // Parameter is Optional - not ideal + // Bad: Calling .get() without isPresent() check - can throw NoSuchElementException + if (optValue.isPresent()) { // This check is good, but often people forget it + return optValue.get(); + } + return "default"; // Or one might just call optValue.get() directly + } + + public void process(String value) { + Optional optionalValue = Optional.ofNullable(value); + // Bad: Using isPresent() and get() where orElse/map could be used + if (optionalValue.isPresent()) { + String s = optionalValue.get(); + System.out.println("Value is: " + s.toUpperCase()); + } else { + System.out.println("Value is: DEFAULT"); + } + } + + // Optional field - generally not recommended + private Optional configuration = Optional.empty(); + public void setConfiguration(String config) { this.configuration = Optional.ofNullable(config); } +} +``` + +## Rule 4: Date/Time API (java.time) + +Title: Utilize the Modern java.time API for Dates and Times +Description: Replace legacy `java.util.Date`, `java.util.Calendar`, and `java.text.SimpleDateFormat` with the classes from the `java.time` package (introduced in Java 8). Choose the appropriate `java.time` class for your specific needs: `Instant` for machine time, `LocalDate` for dates without time-zone, `LocalDateTime` for date-time without time-zone, `ZonedDateTime` for date-time with specific time-zone, `Duration` for time-based amounts, and `Period` for date-based amounts. + +**Good example:** + +```java +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +public class DateTimeApiExample { + public static void main(String[] args) { + // Current date and time + LocalDateTime now = LocalDateTime.now(); + System.out.println("Current LocalDateTime: " + now); + + // Date-specific operations + LocalDate today = LocalDate.now(); + LocalDate tomorrow = today.plusDays(1); + System.out.println("Today: " + today + ", Tomorrow: " + tomorrow); + Period period = Period.between(today, today.plusMonths(2).plusDays(5)); + System.out.println("Period of 2 months and 5 days: " + period); + + // Time-specific operations with time zones + ZonedDateTime zonedNowUTC = ZonedDateTime.now(ZoneId.of("UTC")); + ZonedDateTime zonedNowParis = ZonedDateTime.now(ZoneId.of("Europe/Paris")); + System.out.println("Current time in UTC: " + zonedNowUTC); + System.out.println("Current time in Paris: " + zonedNowParis); + + // Machine time (timestamp) + Instant timestamp = Instant.now(); + System.out.println("Current Instant (UTC): " + timestamp); + + // Duration between two points in time + Instant start = Instant.now(); + try { Thread.sleep(100); } catch (InterruptedException e) { /* ignore */ } + Instant end = Instant.now(); + Duration duration = Duration.between(start, end); + System.out.println("Duration of operation: " + duration.toMillis() + " ms"); + + // Formatting and Parsing + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + String formattedNow = now.format(formatter); + System.out.println("Formatted LocalDateTime: " + formattedNow); + LocalDateTime parsedDateTime = LocalDateTime.parse("2023-01-15 10:30:00", formatter); + System.out.println("Parsed LocalDateTime: " + parsedDateTime); + } +} +``` + +**Bad example:** + +```java +import java.util.Date; +import java.util.Calendar; +import java.text.SimpleDateFormat; + +public class LegacyDateTime { + public static void main(String[] args) { + // Bad: Using legacy java.util.Date (mutable and confusing) + Date oldDate = new Date(); + System.out.println("Legacy Date: " + oldDate); + // oldDate.setMonth(5); // Deprecated and error-prone + + // Bad: Using java.util.Calendar (mutable and cumbersome API) + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_MONTH, 1); + Date tomorrowOld = calendar.getTime(); + System.out.println("Legacy Calendar for tomorrow: " + tomorrowOld); + + // Bad: SimpleDateFormat is not thread-safe + SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); + String formattedOldDate = sdf.format(oldDate); + System.out.println("Legacy formatted date: " + formattedOldDate); + // If sdf were shared among threads, it could lead to incorrect parsing/formatting. + } +} +``` + +## Rule 5: Default Methods in Interfaces + +Title: Enhance Interfaces with Default Methods for Non-Breaking Evolution +Description: Use default methods to add new functionality to existing interfaces without breaking implementing classes. Keep default method implementations simple and focused. Complex logic might be better suited for helper classes or abstract base classes. Clearly document the behavior of default methods, including any assumptions they make about the interface's contract. Avoid introducing state (fields) into interfaces, as default methods cannot access instance fields directly. + +**Good example:** + +```java +interface DataProcessorInterface { + String process(String data); + + // Good: Default method providing optional, additive behavior + default String processWithLogging(String data) { + String threadName = Thread.currentThread().getName(); + System.out.println("" + threadName + " Default Log: Starting to process data - " + data.substring(0, Math.min(10, data.length())) + "..."); + String result = process(data); // Calls the abstract method + System.out.println("" + threadName + " Default Log: Finished processing. Result - " + result.substring(0, Math.min(10, result.length())) + "..."); + return result; + } + + // Another default method + default boolean isValid(String data) { + return data != null && !data.trim().isEmpty(); + } +} + +class SimpleDataProcessor implements DataProcessorInterface { + @Override + public String process(String data) { + return "PROCESSED:" + data.toUpperCase(); + } +} + +class AdvancedDataProcessor implements DataProcessorInterface { + @Override + public String process(String data) { + return "ADVANCED_PROCESSED:" + new StringBuilder(data).reverse().toString(); + } + + // Optionally override the default method + @Override + public String processWithLogging(String data) { + System.out.println("Advanced Log: Pre-processing " + data); + String result = process(data); + System.out.println("Advanced Log: Post-processing done."); + return result; + } +} +``` + +**Bad example:** + +```java +interface BadInterface { + void coreAction(); + + // Bad: Default method with overly complex logic or many dependencies + // that should ideally be in a separate class or abstract class. + default String processWithComplexLogic(String input) { + // Too much logic for a default method + StringBuilder result = new StringBuilder(); + String[] parts = input.split(","); + for (String part : parts) { + if (part.trim().length() > 0) { + result.append(part.trim().toUpperCase()); + if (!part.equals(parts[parts.length - 1])) { + result.append("|"); + } + } + } + // Complex validation + if (result.length() > 100) { + throw new IllegalArgumentException("Result too long"); + } + return result.toString(); + } +} +``` + +## Rule 6: Local Variable Type Inference (var) + +Title: Use var judiciously to improve readability +Description: Use the `var` keyword (introduced in Java 10) for local variable declarations when the type is obvious from the context and it improves readability. Avoid using `var` when it makes the code less clear or when the inferred type might not be what you expect. Use `var` with care in complex expressions or when the type provides important documentation value. + +**Good example:** + +```java +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class VarExample { + public void demonstrateGoodVarUsage() { + // Good: Type is obvious from right-hand side + var names = List.of("Alice", "Bob", "Charlie"); + var nameCount = names.size(); + var isEmpty = names.isEmpty(); + + // Good: Reduces verbosity for complex generic types + var namesByLength = names.stream() + .collect(Collectors.groupingBy(String::length)); + + // Good: Constructor call makes type obvious + var stringBuilder = new StringBuilder(); + var file = new java.io.File("data.txt"); + + // Good: Method return type is clear from method name + var currentUser = getCurrentUser(); + var configuration = loadConfiguration(); + } + + private User getCurrentUser() { return new User(); } + private Configuration loadConfiguration() { return new Configuration(); } +} + +class User {} +class Configuration {} +``` + +**Bad example:** + +```java +import java.util.List; + +public class VarAntiPattern { + public void demonstrateBadVarUsage() { + // Bad: Type is not obvious, could be List or List + var data = getData(); + + // Bad: Return type is ambiguous + var result = processData(data); + + // Bad: Numeric literal type is not obvious (int? long? double?) + var number = 42; + var decimal = 3.14; + + // Bad: Method chain makes type unclear + var processed = data.stream() + .filter(item -> item != null) + .map(item -> item.toString()) + .findFirst(); + + // Bad: Using var with null (compilation error) + // var nullValue = null; // This won't compile + + // Bad: Diamond operator with var is redundant + // var list = new ArrayList<>(); // Type can't be inferred properly + } + + private List getData() { return List.of(); } + private Object processData(List data) { return new Object(); } +} +``` + +## Rule 7: Collection Factory Methods + +Title: Utilize Unmodifiable Collection Factory Methods +Description: Use the static factory methods `List.of()`, `Set.of()`, and `Map.of()` (Java 9+) to create compact, unmodifiable (immutable) collections when the elements are known at compile time. For creating mutable collections, continue to use traditional constructors (e.g., `new ArrayList<>()`) or Stream API collectors. Be aware that these factory methods create unmodifiable collections, do not permit `null` elements, and `Map.of()` does not permit duplicate keys. + +**Good example:** + +```java +import java.util.List; +import java.util.Set; +import java.util.Map; +import java.util.stream.Stream; +import java.util.stream.Collectors; +import java.util.ArrayList; // For mutable list comparison + +public class CollectionFactoryExample { + public static void main(String[] args) { + // Good: Creating unmodifiable (immutable) collections + List unmodifiableList = List.of("alpha", "beta", "gamma"); + System.out.println("Unmodifiable List: " + unmodifiableList); + try { + unmodifiableList.add("delta"); // Throws UnsupportedOperationException + } catch (UnsupportedOperationException e) { + System.out.println("Error adding to unmodifiableList: " + e.getMessage()); + } + + Set unmodifiableSet = Set.of(10, 20, 30, 20); // Duplicate 20 is ignored for Set + System.out.println("Unmodifiable Set: " + unmodifiableSet); + + Map unmodifiableMap = Map.of( + "one", 1, + "two", 2, + "three", 3 + ); + System.out.println("Unmodifiable Map: " + unmodifiableMap); + + // Good: Creating mutable collections using traditional methods or collectors + List mutableListFromStream = Stream.of("x", "y", "z") + .filter(s -> s.length() == 1) + .collect(Collectors.toList()); // Creates a mutable ArrayList by default + mutableListFromStream.add("a"); + System.out.println("Mutable list from stream: " + mutableListFromStream); + + List anotherMutableList = new ArrayList<>(List.of("initial")); // Initialize mutable from unmodifiable + anotherMutableList.add("added"); + System.out.println("Mutable list initialized from List.of: " + anotherMutableList); + } +} +``` + +**Bad example:** + +```java +import java.util.List; +import java.util.Set; +import java.util.Map; + +public class CollectionFactoryAntiPattern { + public static void main(String[] args) { + // Bad: Attempting to use List.of() when null elements are needed (will throw NullPointerException) + try { + List listWithNull = List.of("apple", null, "banana"); + System.out.println(listWithNull); + } catch (NullPointerException e) { + System.err.println("Error: List.of() does not accept null elements. " + e.getMessage()); + } + + // Bad: Attempting to use Map.of() with duplicate keys (will throw IllegalArgumentException) + try { + Map mapWithDuplicateKeys = Map.of("a", 1, "b", 2, "a", 3); + System.out.println(mapWithDuplicateKeys); + } catch (IllegalArgumentException e) { + System.err.println("Error: Map.of() does not accept duplicate keys. " + e.getMessage()); + } + + // Misunderstanding: Thinking List.of() returns a general-purpose mutable list + List myList = List.of("one", "two"); + // myList.add("three"); // This would throw UnsupportedOperationException at runtime + // If mutability is needed, ArrayList should be used: + // List mutableMyList = new ArrayList<>(List.of("one", "two")); + // mutableMyList.add("three"); + } +} +``` + +## Rule 8: CompletableFuture for Asynchronous Programming + +Title: Employ CompletableFuture for Composable Asynchronous Operations +Description: Use `CompletableFuture` (Java 8+) for managing sequences of asynchronous operations, avoiding callback hell and enabling a more functional, composable style. Chain asynchronous tasks using methods like `thenApplyAsync()`, `thenComposeAsync()`, `thenAcceptAsync()`, `thenRunAsync()`. Combine results from multiple `CompletableFuture` instances using `allOf()` or `anyOf()`. Handle exceptions gracefully using `exceptionally()` or `handle()`. Be mindful of the `Executor` used for each stage and consider timeout handling for asynchronous operations. + +**Good example:** + +```java +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +public class CompletableFutureExample { + + private static final ExecutorService customExecutor = Executors.newFixedThreadPool(4); + + // Simulate fetching data asynchronously + private static String fetchData(String query) { + System.out.println("" + Thread.currentThread().getName() + " Fetching data for: " + query); + try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return "ERROR_FETCH";} + if (query.equals("fail_fetch")) throw new RuntimeException("Simulated fetch failure"); + return "Data_for_" + query; + } + + // Simulate processing data + private static String processData(String rawData) { + System.out.println("" + Thread.currentThread().getName() + " Processing data: " + rawData); + try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return "ERROR_PROCESS";} + if (rawData.contains("fail_process")) throw new RuntimeException("Simulated process failure"); + return "Processed_" + rawData; + } + + // Simulate saving data + private static void saveData(String processedData) { + System.out.println("" + Thread.currentThread().getName() + " Saving data: " + processedData); + try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } + System.out.println("" + Thread.currentThread().getName() + " Save complete for: " + processedData); + } + + public static void main(String[] args) { + System.out.println("Starting asynchronous operations..."); + + CompletableFuture futureSuccess = CompletableFuture + .supplyAsync(() -> fetchData("query1"), customExecutor) // Stage 1 on custom executor + .thenApplyAsync(data -> processData(data), customExecutor) // Stage 2 on custom executor + .thenAcceptAsync(result -> saveData(result), customExecutor) // Stage 3 on custom executor + .exceptionally(ex -> { // Handle exceptions from any preceding stage + System.err.println("" + Thread.currentThread().getName() + " Error in chain: " + ex.getMessage()); + return null; // Must return Void (or a compatible type for thenAccept) + }); + + CompletableFuture futureFetchFail = CompletableFuture + .supplyAsync(() -> fetchData("fail_fetch"), customExecutor) + .thenApplyAsync(data -> processData(data), customExecutor) + .thenAcceptAsync(result -> saveData(result), customExecutor) + .exceptionally(ex -> { + System.err.println("" + Thread.currentThread().getName() + " Error in fetch_fail chain: " + ex.getMessage()); + return null; + }); + + System.out.println("Futures submitted. Main thread continues..."); + + // Wait for futures to complete for demonstration purposes + futureSuccess.join(); + futureFetchFail.join(); + + customExecutor.shutdown(); + try { + if (!customExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + customExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + customExecutor.shutdownNow(); + } + System.out.println("All operations finished."); + } +} +``` + +**Bad example:** + +```java +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public class CompletableFutureAntiPattern { + public static void main(String[] args) { + // Bad: Blocking directly on future.get() without timeout in main/request threads + // This negates the benefits of asynchronous programming if not handled carefully. + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + try { Thread.sleep(2000); } catch (InterruptedException e) {} + return "Slow result"; + }); + + String result = null; + try { + System.out.println("Blocking to get future result..."); + result = future.get(); // Blocking call - can make application unresponsive + System.out.println("Got result: " + result); + } catch (InterruptedException | ExecutionException e) { + System.err.println("Error getting future result: " + e.getMessage()); + } + + // Bad: Forgetting to handle exceptions within the CompletableFuture chain + CompletableFuture errorFuture = CompletableFuture.supplyAsync(() -> { + if (true) throw new RuntimeException("Simulated async error!"); + return "Won't reach here"; + }).thenApply(s -> s.toUpperCase()); // This stage might not run, or exception propagates + + try { + errorFuture.join(); // Will throw CompletionException here if not handled by .exceptionally() + } catch (Exception e) { + // assertThat(e.getCause()).isInstanceOf(RuntimeException.class).hasMessage("Processing failed"); + System.out.println("Error from errorFuture: " + e.getClass().getName() + ": "+ e.getCause().getMessage()); + } + // An .exceptionally() or .handle() should be used in the chain. + } +} +``` + +## Rule 9: Module System (Java 9+) + +Title: Leverage the Java Platform Module System (JPMS) for Strong Encapsulation +Description: For applications built on Java 9 or later, consider designing them as modules to achieve strong encapsulation and reliable configuration. Create a `module-info.java` file at the root of your source code to declare your module. Use `requires` clauses to specify dependencies, `exports` clauses to make packages publicly available, `opens` clauses for reflection access, and `provides ... with ...` for service discovery. Carefully consider which packages to export to maintain good encapsulation. + +**Good example:** + +```java +// In src/com.example.application/module-info.java +/* +module com.example.application { + // Depends on the standard java.base and java.logging modules + requires java.base; // Usually implicit but good to be explicit + requires java.logging; + + // Depends on an external library module (assuming it's modularized) + // requires org.example.somelibrary; + + // Exports its API package for other modules to use + exports com.example.application.api; + + // If it uses a service defined in another module + // uses com.example.spi.SomeService; + + // If it provides an implementation of a service + // provides com.example.spi.AnotherService with com.example.application.internal.AnotherServiceImpl; +} +*/ + +// In src/com.example.library/module-info.java (A hypothetical library) +/* +module com.example.library { + requires java.base; + exports com.example.library.utils; +} +*/ + +// Example Java class within com.example.application.api +// package com.example.application.api; +// public class AppService { public String getGreeting() { return "Hello from AppService"; } } + +public class ModuleSystemExample { + public static void main(String[] args) { + System.out.println("This example primarily shows conceptual module-info.java files."); + System.out.println("To run a modular application, compile with module path and run with --module-path and --module."); + // AppService app = new AppService(); + // System.out.println(app.getGreeting()); + } +} +``` + +**Bad example:** + +```java +// In module-info.java +/* +module com.example.badmodule { + // Bad: Exporting too many internal packages, breaking encapsulation + exports com.example.badmodule.internal.utils; + exports com.example.badmodule.internal.anotherpackage; + exports com.example.badmodule.api; // This one might be okay + + // Bad: Requiring transitive on everything by default, can lead to a less stable module graph + // requires transitive java.sql; + // requires transitive com.another.library; + // (Use 'requires transitive' only when your module's API exposes types from the transitive module) +} +*/ +public class BadModuleExample { + public static void main(String[] args) { + System.out.println("Illustrates bad practices in module-info.java like over-exporting internals."); + } +} +``` + +## Rule 10: Performance Considerations with Modern Features + +Title: Be Mindful of Performance Implications of Modern Java Features +Description: Always profile your application before attempting optimizations. Avoid premature optimization. Choose appropriate data structures for the task. Streams can sometimes have overhead compared to simple loops for very small collections. Parallel streams can improve performance for CPU-bound tasks on large datasets but can degrade performance if misused. `Optional` can add object allocation overhead. Lazy initialization should be implemented correctly using double-checked locking or suppliers. + +**Good example:** + +```java +import java.util.List; +import java.util.stream.IntStream; +import java.util.ArrayList; +import java.util.Objects; + +// Simulate an expensive object to initialize +class ExpensiveObject { + public ExpensiveObject() { + System.out.println("ExpensiveObject created!"); + try { Thread.sleep(100); } catch (InterruptedException e) {} // Simulate costly creation + } + public void use() { System.out.println("ExpensiveObject used."); } +} + +public class PerformanceConsiderations { + // Good: Lazy initialization using double-checked locking for a singleton-like expensive resource + private volatile ExpensiveObject instance; + + public ExpensiveObject getLazyInitializedInstance() { + ExpensiveObject result = instance; // Read volatile field once + if (Objects.isNull(result)) { + synchronized (this) { // Synchronize only if instance is null + result = instance; // Double-check + if (Objects.isNull(result)) { + instance = result = new ExpensiveObject(); + } + } + } + return result; + } + + public static void main(String[] args) { + PerformanceConsiderations pc = new PerformanceConsiderations(); + System.out.println("Getting instance first time:"); + ExpensiveObject obj1 = pc.getLazyInitializedInstance(); + obj1.use(); + + System.out.println("\nGetting instance second time (should be cached):"); + ExpensiveObject obj2 = pc.getLazyInitializedInstance(); + obj2.use(); + System.out.println("obj1 == obj2: " + (obj1 == obj2)); + + // Regarding streams: For very large collections and CPU-bound tasks, parallel streams can help. + // But always measure. Example: + final int LIST_SIZE = 1_000_000; + List numbers = new ArrayList<>(LIST_SIZE); + for(int i=0; i (long)i*i).sum(); // Squaring, CPU intensive + long endTime = System.nanoTime(); + System.out.println("\nSequential sum of squares: " + sumSequential + " in " + (endTime-startTime)/1_000_000 + "ms"); + + startTime = System.nanoTime(); + long sumParallel = numbers.parallelStream().mapToLong(i -> (long)i*i).sum(); + endTime = System.nanoTime(); + System.out.println("Parallel sum of squares: " + sumParallel + " in " + (endTime-startTime)/1_000_000 + "ms"); + // On multi-core machines, parallel *may* be faster here. + } +} +``` + +**Bad example:** + +```java +import java.util.List; +import java.util.ArrayList; +import java.util.stream.Collectors; + +public class PerformanceAntiPattern { + private ExpensiveObject costlyObject; // Eagerly initialized, even if not used + + public PerformanceAntiPattern() { + // Bad: Eagerly initializing a very expensive object that might not be needed. + // this.costlyObject = new ExpensiveObject(); + // System.out.println("Costly object created in constructor regardless of use."); + } + + public void sometimesUseCostlyObject() { + // If costlyObject was initialized eagerly, its cost is paid even if this method isn't called often. + // if (costlyObject != null && Math.random() > 0.8) costlyObject.use(); + System.out.println("Costly object would have been initialized in constructor if uncommented."); + } + + public static void main(String[] args) { + PerformanceAntiPattern pap = new PerformanceAntiPattern(); + pap.sometimesUseCostlyObject(); + + List smallList = List.of(1, 2, 3, 4, 5); + // Bad: Using parallel stream for a tiny list and simple operation. + // Overhead of parallelization likely exceeds any benefit. + System.out.println("\nCalculating sum of small list (parallel, likely inefficient):"); + long sum = smallList.parallelStream().mapToInt(Integer::intValue).sum(); + System.out.println("Sum: " + sum); + } +} +``` + +## Rule 11: Testing Modern Java Code + +Title: Adapt Testing Strategies for Modern Java Features +Description: Adapt testing strategies to properly test modern Java features. Focus on testing behavior rather than implementation when testing lambda expressions and functional code. Use appropriate mocking strategies for functional interfaces and streams. Test asynchronous code with CompletableFuture properly, including timeout scenarios and exception handling. Ensure proper testing coverage of Optional usage patterns and edge cases. Utilize modern testing frameworks and assertion libraries that integrate well with modern Java features. + +**Good example:** + +```java +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +// For actual tests, use JUnit, TestNG, AssertJ etc. +// import org.junit.jupiter.api.Test; +// import static org.assertj.core.api.Assertions.assertThat; +// import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ModernFeaturesService { + public List filterAndToUpper(List input, String startsWith) { + if (Objects.isNull(input)) return List.of(); + return input.stream() + .filter(s -> s != null && s.startsWith(startsWith)) + .map(String::toUpperCase) + .collect(Collectors.toList()); + } + + public Optional findFirstLongString(List input, int minLength) { + if (Objects.isNull(input)) return Optional.empty(); + return input.stream() + .filter(s -> s != null && s.length() >= minLength) + .findFirst(); + } + + public CompletableFuture processDataAsync(String data) { + return CompletableFuture.supplyAsync(() -> { + try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } + if (data.equals("fail")) throw new RuntimeException("Processing failed"); + return "PROCESSED:" + data; + }); + } +} + +// Conceptual Tests (using System.out for assertions for simplicity here) +public class TestingModernCodeExample { + + // @Test // Example with AssertJ style (if library was present) + void testFilterAndToUpper_withAssertJ() { + ModernFeaturesService service = new ModernFeaturesService(); + List input = List.of("apple", "apricot", "banana", "avocado"); + List result = service.filterAndToUpper(input, "ap"); + // assertThat(result).containsExactlyInAnyOrder("APPLE", "APRICOT"); + System.out.println("testFilterAndToUpper: " + result); // Expected: APPLE, APRICOT + } + + // @Test + void testFindFirstLongString_found_withAssertJ() { + ModernFeaturesService service = new ModernFeaturesService(); + List input = List.of("short", "verylongstring", "medium"); + Optional result = service.findFirstLongString(input, 10); + // assertThat(result).isPresent().hasValue("verylongstring"); + System.out.println("testFindFirstLongString (found): " + result); // Expected: Optionalverylongstring + } + + // @Test + void testFindFirstLongString_notFound_withAssertJ() { + ModernFeaturesService service = new ModernFeaturesService(); + List input = List.of("short", "medium"); + Optional result = service.findFirstLongString(input, 10); + // assertThat(result).isEmpty(); + System.out.println("testFindFirstLongString (not found): " + result); // Expected: Optional.empty + } + + // @Test + void testProcessDataAsync_success() { + ModernFeaturesService service = new ModernFeaturesService(); + CompletableFuture future = service.processDataAsync("test"); + // In a real test, use Awaitility or future.join() with try-catch for CompletionException + try { + String result = future.get(); // Blocking for example, use non-blocking in real tests + // assertThat(result).isEqualTo("PROCESSED:test"); + System.out.println("testProcessDataAsync (success): " + result); // Expected: PROCESSED:test + } catch (Exception e) { e.printStackTrace(); } + } + + // @Test + void testProcessDataAsync_failure() { + ModernFeaturesService service = new ModernFeaturesService(); + CompletableFuture future = service.processDataAsync("fail"); + // assertThatThrownBy(future::join).isInstanceOf(CompletionException.class); + try { + future.join(); // This will throw CompletionException + } catch (Exception e) { + // assertThat(e.getCause()).isInstanceOf(RuntimeException.class).hasMessage("Processing failed"); + System.out.println("testProcessDataAsync (failure expected): " + e.getClass().getName() + " -> " + e.getCause().getMessage()); + } + } + + public static void main(String[] args) { + System.out.println("These are conceptual tests. Use a testing framework for real scenarios."); + TestingModernCodeExample tests = new TestingModernCodeExample(); + tests.testFilterAndToUpper_withAssertJ(); + tests.testFindFirstLongString_found_withAssertJ(); + tests.testFindFirstLongString_notFound_withAssertJ(); + tests.testProcessDataAsync_success(); + tests.testProcessDataAsync_failure(); + } +} +``` + +**Bad example:** + +```java +import java.util.List; +import java.util.Optional; + +// Bad: Not testing edge cases or different lambda behaviors +public class InsufficientTesting { + + public Optional processList(List data) { + // Complex logic that should be thoroughly tested + return data.stream() + .filter(s -> s.length() > 5) + .map(s -> s.substring(0, 5)) + .findFirst(); + } + + public static void main(String[] args) { + InsufficientTesting it = new InsufficientTesting(); + // Only testing the "happy path" + Optional result = it.processList(List.of("longstring", "another")); + System.out.println("Result (happy path only): " + result.orElse("NOTHING")); + + // Not tested: + // - Empty list input + // - List with no strings matching filter + // - List with nulls (if stream pipeline doesn't handle them) + // - Performance with very large lists + // This lack of thorough testing can lead to bugs in production. + } +} +``` + +## Rule 12: Use Text Blocks for Readable Multi-line Strings + +Title: Employ Text Blocks for Clear and Maintainable Multi-line String Literals +Description: Utilize text blocks (`"""..."""`) to represent multi-line string literals in a way that preserves indentation and formatting, making them easier to read and write, especially for embedded code snippets like JSON, XML, SQL, or HTML. Text blocks automatically handle newline characters and manage indentation. Incidental leading white space is automatically stripped from each line. You can control trailing white space and use escape sequences as needed. + +**Good example:** + +```java +public class TextBlockExample { + public static void main(String[] args) { + // Good: HTML snippet using a text block + String html = """ + + +

Hello, Java Text Blocks!

+ + + """; + System.out.println("HTML:\n" + html); + + // Good: JSON snippet with preserved indentation + String json = """ + { + "name": "Java Text Block", + "feature": "Multi-line strings", + "since": 15 + } + """; + System.out.println("JSON:\n" + json); + + // Good: SQL query + String query = """ + SELECT id, name, email + FROM users + WHERE department = 'Engineering' + ORDER BY name; + """; + System.out.println("SQL Query:\n" + query); + + // Good: Controlling indentation - the content's indentation relative + // to the closing """ is preserved. + String indented = """ + Line 1 + Line 2 (more indented) + Line 3 (less indented than line 2, but aligned with Line 1 relative to closing quotes) + """; + System.out.println("Indented Example:\n" + indented); + } +} +``` + +**Bad example:** + +```java +public class OldMultiLineStringExample { + public static void main(String[] args) { + // Bad: Using traditional string concatenation for multi-line text + String htmlOld = "\n" + + " \n" + + "

Hello, old Java strings!

\n" + + " \n" + + "\n"; + System.out.println("Old HTML:\n" + htmlOld); + + // Bad: Hard to read and maintain SQL query + String queryOld = "SELECT id, name, email\n" + + "FROM users\n" + + "WHERE department = 'Engineering'\n" + + "ORDER BY name;\n"; + System.out.println("Old SQL Query:\n" + queryOld); + + // Bad: Incorrectly trying to manage indentation within a text block + // by having significant content on the same line as opening """ + // (This can work, but it's less clear than starting content on new line) + String mixedStyle = """ This is the first line. + This is the second line. + """; // The indentation is determined by the least indented line or the closing """ + System.out.println("Mixed Style (potentially confusing indentation):\n" + mixedStyle); + } +} +``` \ No newline at end of file diff --git a/spml/src/test/resources/142-java-functional-programming.mdc b/spml/src/test/resources/142-java-functional-programming.mdc new file mode 100644 index 00000000..385dacfe --- /dev/null +++ b/spml/src/test/resources/142-java-functional-programming.mdc @@ -0,0 +1,559 @@ +--- +description: +globs: +alwaysApply: false +--- +# Java Functional Programming rules + +## System prompt characterization + +Role definition: You are a Senior software engineer with extensive experience in Java software development + +## Description + +Java functional programming revolves around immutable objects and state transformations, ensuring functions are pure (no side effects, depend only on inputs). It leverages functional interfaces, concise lambda expressions, and the Stream API for collection processing. Core paradigms include function composition, `Optional` for null safety, and higher-order functions. Modern Java features like Records enhance immutable data transfer, while pattern matching (for `instanceof` and `switch`) and switch expressions improve conditional logic. Sealed classes and interfaces enable controlled, exhaustive hierarchies, and upcoming Stream Gatherers will offer advanced custom stream operations. + +## Table of contents + +- Rule 1: Immutable Objects +- Rule 2: State Immutability +- Rule 3: Pure Functions +- Rule 4: Functional Interfaces +- Rule 5: Lambda Expressions +- Rule 6: Streams +- Rule 7: Functional Programming Paradigms +- Rule 8: Leverage Records for Immutable Data Transfer +- Rule 9: Employ Pattern Matching for `instanceof` and `switch` +- Rule 10: Use Switch Expressions for Concise Multi-way Conditionals +- Rule 11: Leverage Sealed Classes and Interfaces for Controlled Hierarchies +- Rule 12: Explore Stream Gatherers for Custom Stream Operations + +## Rule 1: Immutable Objects + +Title: Ensure Objects are Immutable +Description: - Use `final` classes and fields. - Initialize all fields in the constructor. - Do not provide setter methods. - Return defensive copies of mutable fields (e.g., collections, dates) when exposing them via getters. + +**Good example:** + +```java +import java.util.List; +import java.util.ArrayList; + +public final class Person { + private final String name; + private final int age; + private final List hobbies; // Make it List, not ArrayList + + public Person(String name, int age, List hobbies) { + this.name = name; + this.age = age; + // Ensure the incoming list is defensively copied to an immutable list + this.hobbies = List.copyOf(hobbies); + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + // Return an immutable view or a defensive copy + public List getHobbies() { + return this.hobbies; // List.copyOf already returns an unmodifiable list + } +} + +``` + +## Rule 2: State Immutability + +Title: Prefer Immutable State Transformations +Description: - Instead of modifying existing objects, return new objects representing the new state. - Utilize collectors that produce immutable collections (e.g., `Collectors.toUnmodifiableList()`). - Leverage immutable collection types provided by libraries or Java itself. + +**Good example:** + +```java +import java.util.List; +import java.util.stream.Collectors; + +public class PriceCalculator { + public static List applyDiscount(List prices, double discount) { + return prices.stream() + .map(price -> price * (1 - discount)) + .collect(Collectors.toUnmodifiableList()); // Ensures the returned list is immutable + } +} + +``` + +## Rule 3: Pure Functions + +Title: Write Pure Functions +Description: - Functions should depend only on their input parameters and not on any external or hidden state. - They should not cause any side effects (e.g., modifying external variables, I/O operations). - Given the same input, a pure function must always return the same output. - Avoid modifying external state or relying on it. + +**Good example:** + +```java +import java.util.List; +import java.util.stream.Collectors; + +public class MathOperations { + // Pure function: depends only on input, no side effects + public static int add(int a, int b) { + return a + b; + } + + // Pure function: transforms input list to a new list without modifying the original + public static List doubleNumbers(List numbers) { + return numbers.stream() + .map(n -> n * 2) + .collect(Collectors.toList()); // Could also be toUnmodifiableList() + } +} + +``` + +## Rule 4: Functional Interfaces + +Title: Utilize Functional Interfaces Effectively +Description: - Prefer built-in functional interfaces from `java.util.function` (e.g., `Function`, `Predicate`, `Consumer`, `Supplier`, `UnaryOperator`) when they suit the need. - Create custom functional interfaces (annotated with `@FunctionalInterface`) for specific, clearly defined single abstract methods. - Keep functional interfaces focused on a single responsibility. + +**Good example:** + +```java +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.time.LocalDateTime; + +// Built-in functional interfaces +class FunctionalInterfaceExamples { + Function stringToLength = String::length; + Predicate isEven = n -> n % 2 == 0; + Consumer printer = System.out::println; + Supplier now = LocalDateTime::now; +} + +// Custom functional interface +@FunctionalInterface +interface Validator { + boolean validate(T value); +} + +``` + +## Rule 5: Lambda Expressions + +Title: Employ Lambda Expressions Clearly and Concisely +Description: - Use method references (e.g., `String::length`, `System.out::println`) when they are clearer and more concise than an equivalent lambda expression. - Keep lambda expressions short and focused on a single piece of logic to maintain readability. - Extract complex or multi-line lambda logic into separate, well-named private methods. + +**Good example:** + +```java +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class LambdaExamples { + public static void main(String[] args) { + List names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve"); + + // Method reference for conciseness + names.forEach(System.out::println); + + // Simple, readable lambda + List longNames = names.stream() + .filter(name -> name.length() > 4) + .collect(Collectors.toList()); + + // Complex logic extracted to a private helper method + List validNames = names.stream() + .filter(LambdaExamples::isValidName) + .collect(Collectors.toList()); + + System.out.println("Long names: " + longNames); + System.out.println("Valid names: " + validNames); + } + + // Helper method for more complex lambda logic + private static boolean isValidName(String name) { + return name.length() > 3 && Character.isUpperCase(name.charAt(0)); + } +} + +``` + +## Rule 6: Streams + +Title: Leverage Streams for Collection Processing +Description: - Use the Stream API for processing sequences of elements from collections or other sources. - Chain stream operations (intermediate operations like `filter`, `map`, `sorted`) to create a pipeline for complex transformations. - Consider using parallel streams (`collection.parallelStream()`) for potentially improved performance on large datasets, but be mindful of the overhead and suitability for the task. - Choose appropriate terminal operations (e.g., `collect`, `forEach`, `reduce`, `findFirst`, `anyMatch`) to produce a result or side-effect. + +**Good example:** + +```java +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class StreamExamples { + public static void main(String[] args) { + List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + // Basic stream operations: filter even numbers and square them + List evenSquares = numbers.stream() + .filter(n -> n % 2 == 0) + .map(n -> n * n) + .collect(Collectors.toList()); + System.out.println("Even squares: " + evenSquares); + + // Advanced stream operations: partitioning numbers + Map> partitionedByGreaterThanFive = numbers.stream() + .collect(Collectors.partitioningBy(n -> n > 5)); + System.out.println("Partitioned by > 5: " + partitionedByGreaterThanFive); + + // Parallel stream for calculating average (use with caution, consider dataset size) + double average = numbers.parallelStream() + .mapToDouble(Integer::doubleValue) + .average() + .orElse(0.0); + System.out.println("Average: " + average); + } +} + +``` + +## Rule 7: Functional Programming Paradigms + +Title: Apply Core Functional Programming Paradigms +Description: - **Function Composition**: Combine simpler functions to create more complex ones. Use `Function.compose()` and `Function.andThen()`. - **Optional for Null Safety**: Use `Optional` to represent values that may be absent, avoiding `NullPointerExceptions` and clearly signaling optionality. - **Recursion**: Implement algorithms using recursion where it naturally fits the problem (e.g., tree traversal), especially tail recursion if supported or optimized by the JVM. - **Higher-Order Functions**: Utilize functions that accept other functions as arguments or return them as results (e.g., `Stream.map`, `Stream.filter`). + +**Good example:** + +```java +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.IntStream; + +public class FunctionalParadigms { + + // Function composition + public static void demonstrateComposition() { + Function intToString = Object::toString; + Function stringLength = String::length; + // Executes intToString first, then stringLength + Function composedLengthAfterToString = stringLength.compose(intToString); + System.out.println("Composed (123 -> length): " + composedLengthAfterToString.apply(123)); // Output: 3 + } + + // Optional usage for safe division + public static Optional divideNumbers(Double numerator, Double denominator) { + if (Objects.isNull(denominator) || denominator == 0) { + return Optional.empty(); + } + return Optional.of(numerator / denominator); + } + + // Factorial using IntStream (more functional and often safer for large n) + public static long factorialFunctional(int n) { + if (n < 0) throw new IllegalArgumentException("Factorial not defined for negative numbers"); + return IntStream.rangeClosed(1, n) + .asLongStream() // Ensure long for intermediate products + .reduce(1L, (a, b) -> a * b); + } + + // Recursion example: factorial (iterative version often preferred for stack safety in Java) + // Note: Streams provide a more functional way for such operations in many cases. + public static long factorialRecursive(int n) { + if (n < 0) throw new IllegalArgumentException("Factorial not defined for negative numbers"); + if (n == 0 || n == 1) return 1; + return n * factorialRecursive(n - 1); + } + + // Higher-order function: memoization + public static Function memoize(Function function) { + Map cache = new ConcurrentHashMap<>(); + // The returned function closes over the cache + return input -> cache.computeIfAbsent(input, function); + } + + public static void main(String[] args) { + demonstrateComposition(); + + System.out.println("Divide 10 by 2: " + divideNumbers(10.0, 2.0).orElse(Double.NaN)); + System.out.println("Divide 10 by 0: " + divideNumbers(10.0, 0.0).orElse(Double.NaN)); + + System.out.println("Factorial recursive (5): " + factorialRecursive(5)); + System.out.println("Factorial functional (5): " + factorialFunctional(5)); + + Function expensiveOperation = x -> { + System.out.println("Computing for " + x); + try { Thread.sleep(1000); } catch (InterruptedException e) {} + return x * x; + }; + + Function memoizedOp = memoize(expensiveOperation); + System.out.println("Memoized (4): " + memoizedOp.apply(4)); // Computes + System.out.println("Memoized (4): " + memoizedOp.apply(4)); // Returns from cache + System.out.println("Memoized (5): " + memoizedOp.apply(5)); // Computes + } +} + +``` + +## Rule 8: Leverage Records for Immutable Data Transfer + +Title: Leverage Records for Immutable Data Transfer +Description: - Use Records (JEP 395, standardized in Java 16) as the primary way to model simple, immutable data aggregates. - Records automatically provide constructors, getters (accessor methods with the same name as the field), `equals()`, `hashCode()`, and `toString()` methods, reducing boilerplate. - This aligns perfectly with the functional paradigm's preference for immutability and conciseness. + +**Good example:** + +```java +public record PointRecord(int x, int y) { + // Optional: add custom compact constructors, static factory methods, or instance methods. + // By default, all fields are final, and public accessor methods (e.g., x(), y()) are generated. +} + +// Usage: +// PointRecord p = new PointRecord(10, 20); +// int xVal = p.x(); // Accessor method +// int yVal = p.y(); // Accessor method + +``` + +**Bad example:** + +```java +public final class PointClass { + private final int x; + private final int y; + + public PointClass(int x, int y) { + this.x = x; + this.y = y; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (Objects.isNull(o) || getClass() != o.getClass()) return false; + PointClass that = (PointClass) o; + return x == that.x && y == that.y; + } + + @Override + public int hashCode() { + return java.util.Objects.hash(x, y); + } + + @Override + public String toString() { + return "PointClass[" + + "x=" + x + ", " + + "y=" + y + ']'; + } +} + +``` + +## Rule 9: Employ Pattern Matching for `instanceof` and `switch` + +Title: Employ Pattern Matching for Type-Safe Conditional Logic +Description: - Utilize Pattern Matching for `instanceof` to simplify type checks and casts in a single step. - Employ Pattern Matching for `switch` for more expressive and robust conditional logic, especially with sealed types and records. - This reduces boilerplate, improves readability, and enhances type safety. + +**Good example:** + +```java +public String processShapeWithPatternInstanceof(Object shape) { + if (shape instanceof Circle c) { // Type test and binding in one + return "Circle with radius " + c.getRadius(); + } else if (shape instanceof Rectangle r) { + return "Rectangle with width " + r.getWidth() + " and height " + r.getHeight(); + } + return "Unknown shape"; +} + +// Pattern Matching for switch with Records and Sealed Interfaces +sealed interface Shape permits CircleRecord, RectangleRecord, SquareRecord {} +record CircleRecord(double radius) implements Shape {} +record RectangleRecord(double length, double width) implements Shape {} +record SquareRecord(double side) implements Shape {} + +public String processShapeWithPatternSwitch(Shape shape) { + return switch (shape) { + case CircleRecord c -> "Circle with radius " + c.radius(); + case RectangleRecord r -> "Rectangle with length " + r.length() + " and width " + r.width(); + case SquareRecord s -> "Square with side " + s.side(); + // No default needed if all permitted types of the sealed interface are covered + }; +} + +``` + +**Bad example:** + +```java +public String processShapeLegacy(Object shape) { + if (shape instanceof Circle) { + Circle c = (Circle) shape; + return "Circle with radius " + c.getRadius(); + } else if (shape instanceof Rectangle) { + Rectangle r = (Rectangle) shape; + return "Rectangle with width " + r.getWidth() + " and height " + r.getHeight(); + } + return "Unknown shape"; +} + +// Assume Circle and Rectangle classes exist for this example +// class Circle { public double getRadius() { return 0; } } +// class Rectangle { public double getWidth() { return 0; } public double getHeight() { return 0; } } + +``` + +## Rule 10: Use Switch Expressions for Concise Multi-way Conditionals + +Title: Use Switch Expressions for Concise and Safe Multi-way Conditionals +Description: - Prefer Switch Expressions (JEP 361, Java 14) over traditional switch statements for assigning the result of a multi-way conditional to a variable. - Switch expressions are more concise, less error-prone (e.g., no fall-through by default, compiler checks for exhaustiveness with enums/sealed types). - They fit well with functional programming's emphasis on expressions over statements. + +**Good example:** + +```java +public String getDayTypeWithSwitchExpr(String day) { + return switch (day) { + case "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY" -> "Weekday"; + case "SATURDAY", "SUNDAY" -> "Weekend"; + default -> throw new IllegalArgumentException("Invalid day: " + day); + }; +} + +// Example with enum for exhaustive switch +enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY } + +public String getDayCategory(Day day) { + return switch (day) { + case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Weekday"; + case SATURDAY, SUNDAY -> "Weekend"; + // No default needed if all enum constants are covered + }; +} + +``` + +**Bad example:** + +```java +public String getDayTypeLegacy(String day) { + String type; + switch (day) { + case "MONDAY": + case "TUESDAY": + case "WEDNESDAY": + case "THURSDAY": + case "FRIDAY": + type = "Weekday"; + break; + case "SATURDAY": + case "SUNDAY": + type = "Weekend"; + break; + default: + throw new IllegalArgumentException("Invalid day: " + day); + } + return type; +} + +``` + +## Rule 11: Leverage Sealed Classes and Interfaces for Controlled Hierarchies + +Title: Leverage Sealed Classes and Interfaces for Precise Domain Modeling +Description: - Use Sealed Classes and Interfaces (JEP 409, Java 17) to define class/interface hierarchies where all direct subtypes are known, finite, and explicitly listed. - This enables more robust domain modeling and allows the compiler to perform exhaustive checks in pattern matching (e.g., with `switch` expressions), eliminating the need for a default case in many scenarios. - Particularly useful for creating sum types (algebraic data types) which are common in functional programming. + +**Good example:** + +```java +// Define a sealed interface for different types of events +public sealed interface Event permits LoginEvent, LogoutEvent, FileUploadEvent { + long getTimestamp(); +} + +// Define permitted implementations (often records for immutability) +public record LoginEvent(String userId, long timestamp) implements Event { + @Override public long getTimestamp() { return timestamp; } +} + +public record LogoutEvent(String userId, long timestamp) implements Event { + @Override public long getTimestamp() { return timestamp; } +} + +public record FileUploadEvent(String userId, String fileName, long fileSize, long timestamp) implements Event { + @Override public long getTimestamp() { return timestamp; } +} + +// A function processing the sealed hierarchy can be made exhaustive +public class EventProcessor { + public String processEvent(Event event) { + return switch (event) { + case LoginEvent le -> "User " + le.userId() + " logged in at " + le.getTimestamp(); + case LogoutEvent loe -> "User " + loe.userId() + " logged out at " + loe.getTimestamp(); + case FileUploadEvent fue -> "User " + fue.userId() + " uploaded " + fue.fileName() + " at " + fue.getTimestamp(); + // No default case is necessary if the switch is exhaustive for all permitted types of Event. + }; + } +} + +``` + +## Rule 12: Explore Stream Gatherers for Custom Stream Operations + +Title: Explore Stream Gatherers for Advanced Custom Stream Operations +Description: - For complex or highly custom stream processing tasks that are not easily achieved with standard terminal operations or collectors, investigate Stream Gatherers (JEP 461). - Gatherers (`java.util.stream.Gatherer`) allow defining custom intermediate operations, offering more flexibility and power for sophisticated data transformations within functional pipelines. - This feature is aimed at more advanced use cases where reusability and composition of stream operations are key. + +**Good example:** + +```java +import java.util.List; +import java.util.stream.Stream; +// import java.util.stream.Gatherers; // Assuming this is where predefined gatherers might reside + +public class StreamGathererExample { + + // Hypothetical: A custom gatherer that creates sliding windows of elements. + // The actual implementation of such a gatherer would be more involved. + // static Gatherer> windowed(int size) { + // // ... implementation details ... + // return null; // Placeholder + // } + + public static void main(String[] args) { + // List> windows = Stream.of(1, 2, 3, 4, 5, 6, 7) + // .gather(windowed(3)) // Using a hypothetical custom 'windowed' gatherer + // .toList(); + // + // // Expected output might be: [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6], [5, 6, 7]] + // System.out.println(windows); + + System.out.println("Stream Gatherers are a new feature. Refer to official Java documentation for concrete examples and API details as they become available."); + } +} + +// Rule of Thumb: +// Before implementing very complex custom collectors or resorting to imperative loops for intricate stream transformations, +// evaluate if a Stream Gatherer could offer a more declarative, reusable, and composable solution. +// This is for advanced stream users looking to build sophisticated data processing pipelines. + +``` \ No newline at end of file diff --git a/spml/src/test/resources/143-java-data-oriented-programming.mdc b/spml/src/test/resources/143-java-data-oriented-programming.mdc new file mode 100644 index 00000000..c609f730 --- /dev/null +++ b/spml/src/test/resources/143-java-data-oriented-programming.mdc @@ -0,0 +1,865 @@ +--- +description: +globs: +alwaysApply: false +--- +# Java rules to apply data oriented programming style + +## System prompt characterization + +Role definition: You are a Senior software engineer with extensive experience in Java software development + +## Description + +Java Data-Oriented Programming emphasizes separating code (behavior) from data structures, which should ideally be immutable (e.g., using records). Data manipulation should occur via pure functions that transform data into new instances. It's often beneficial to keep data structures flat and denormalized (using IDs for references) where appropriate, and to start with generic data representations (like `Map`) converting to specific types only when necessary. Data integrity is ensured through pure validation functions. Flexible, generic data access layers facilitate working with various data types and storage mechanisms. All data transformations should be explicit, traceable, and composed of clear, pure functional steps. + +## Table of contents + +- Rule 1: Separate Code from Data +- Rule 2: Data Should Be Immutable +- Rule 3: Use Pure Functions to Manipulate Data +- Rule 4: Keep Data Flat and Denormalized +- Rule 5: Keep Data Generic Until Specific +- Rule 6: Data Integrity through Validation Functions +- Rule 7: Flexible and Generic Data Access +- Rule 8: Explicit and Traceable Data Transformation + +## Rule 1: Separate Code from Data + +Title: Decouple Behavior (Code) from Data Structures +Description: - Use records or simple POJOs primarily for holding data. - Place behavior (methods that operate on data) in separate utility classes or services. - Avoid mixing state (fields) and complex behavior (methods with logic) within the same class intended as a data carrier. - Prefer static methods in utility classes for operations on data objects. - Design data structures to be self-contained and focused solely on representing state. + +**Good example:** + +```java +// Data structure (record) +record UserData(String name, int age) {} + +// Behavior in a separate utility class +class UserActions { + public static void validateAge(UserData user) { + if (user.age() < 0) { + throw new IllegalArgumentException("Age cannot be negative: " + user.age()); + } + System.out.println("Age for " + user.name() + " is valid."); + } + + public static UserData activateUser(UserData user) { + // Example transformation logic + System.out.println("Activating user: " + user.name()); + return new UserData(user.name().toUpperCase(), user.age()); // Returns new data + } +} + +class SeparateCodeDataExample { + public static void main(String args) { + UserData user1 = new UserData("Alice", 30); + UserActions.validateAge(user1); + UserData activatedUser1 = UserActions.activateUser(user1); + System.out.println("Activated user: " + activatedUser1); + + try { + UserData user2 = new UserData("Bob", -5); + UserActions.validateAge(user2); // This will throw + } catch (IllegalArgumentException e) { + System.err.println(e.getMessage()); + } + } +} +``` + +**Bad example:** + +```java +// Mixing code and data in one class +class User { + private String name; + private int age; + + public User(String name, int age) { + this.name = name; + this.age = age; + } + + // Behavior mixed with data + public void validateAge() { + if (age < 0) { + throw new IllegalArgumentException("Age cannot be negative: " + age); + } + System.out.println("Age for " + name + " is valid."); + } + + public String getName() { return name; } + public int getAge() { return age; } + + public static void main(String args) { + User user1 = new User("Alice", 30); + user1.validateAge(); + // Problem: User object itself has methods, not just data. + // If User was a record, it couldn't have such instance methods beyond generated ones. + } +} +``` + +## Rule 2: Data Should Be Immutable + +Title: Ensure Data Immutability +Description: - Use records (which are inherently immutable) whenever possible for data carriers. - Declare all fields as `final`. - Do not provide setter methods for fields. - When returning collections or other mutable types from getters, return defensive copies or unmodifiable views (e.g., `List.copyOf()`, `Collections.unmodifiableList()`). - For transformations, always create and return new instances with the modified data rather than altering existing instances. + +**Good example:** + +```java +import java.util.List; +import java.util.ArrayList; + +// Immutable data using record +record ServerConfig(String host, int port, List features) { + // Canonical constructor is implicitly final for fields + // Records provide public accessors (host(), port(), features()) + // equals(), hashCode(), toString() are auto-generated + + // For mutable collections in constructor, ensure immutability + public ServerConfig(String host, int port, List features) { + this.host = host; + this.port = port; + this.features = List.copyOf(features); // Create an immutable copy + } + + // Getter for list returns the immutable copy + @Override + public List features() { + return this.features; // Already an immutable list from List.copyOf + } +} + +class ImmutabilityExample { + public static void main(String args) { + List initialFeatures = new ArrayList<>(); + initialFeatures.add("FeatureA"); + + ServerConfig config1 = new ServerConfig("localhost", 8080, initialFeatures); + System.out.println("Config1: " + config1); + + initialFeatures.add("FeatureB"); // Modify original list + System.out.println("Config1 after modifying original list: " + config1); // config1.features is unaffected + + try { + config1.features().add("FeatureC"); // Should throw UnsupportedOperationException + } catch (UnsupportedOperationException e) { + System.out.println("Attempt to modify config1.features(): " + e.getMessage()); + } + + // Transformation creates a new instance + ServerConfig config2 = new ServerConfig(config1.host(), 9090, config1.features()); + System.out.println("Config2 (new port): " + config2); + System.out.println("Config1 is unchanged: " + config1); + } +} +``` + +**Bad example:** + +```java +import java.util.List; +import java.util.ArrayList; + +// Mutable data class +class MutableConfig { + private String host; + private int port; + private List features; // Mutable list + + public MutableConfig(String host, int port, List features) { + this.host = host; + this.port = port; + this.features = new ArrayList<>(features); // Stores a mutable copy + } + + public void setHost(String host) { this.host = host; } + public void setPort(int port) { this.port = port; } + public void addFeature(String feature) { this.features.add(feature); } // Modifies internal state + + public List getFeatures() { + return this.features; // Returns a reference to the internal mutable list + } + + @Override + public String toString() { + return "MutableConfig{host='" + host + "', port=" + port + ", features=" + features + "}"; + } + + public static void main(String args) { + List initialFeatures = new ArrayList<>(); + initialFeatures.add("InitialFeature"); + MutableConfig mConfig = new MutableConfig("server1", 80, initialFeatures); + System.out.println("mConfig: " + mConfig); + + mConfig.setPort(8080); // State is mutated + mConfig.addFeature("NewFeature"); // State is mutated + System.out.println("mConfig mutated: " + mConfig); + + List gotFeatures = mConfig.getFeatures(); + gotFeatures.add("AnotherFeatureAddedExternally"); // External modification of internal state! + System.out.println("mConfig after external list modification: " + mConfig); + } +} +``` + +## Rule 3: Use Pure Functions to Manipulate Data + +Title: Employ Pure Functions for Data Transformations +Description: - Functions that manipulate data should depend solely on their input parameters, not on any instance or external state. - They must not cause any side effects (e.g., modifying input objects, global variables, I/O operations). - Pure functions are inherently stateless: given the same input, they always produce the same output. - Transformations should return new data instances rather than modifying the input data directly. - Prefer static methods for such pure data transformation functions. - Keep each function focused on a single, well-defined transformation. + +**Good example:** + +```java +import java.util.List; +import java.util.stream.Collectors; +import java.util.ArrayList; + +// Data (can be a record) +record ItemPrice(String itemId, double price) {} + +// Pure functions for operations +class PriceOperations { + // Calculates total price based only on inputs + public static double calculateTotalWithTax(double price, double taxRate) { + if (price < 0 || taxRate < 0) { + throw new IllegalArgumentException("Price and tax rate must be non-negative."); + } + return price * (1 + taxRate); + } + + // Applies a discount and returns a new list of items with updated prices + public static List applyDiscountToItems(List items, double discountPercentage) { + if (discountPercentage < 0 || discountPercentage > 1) { + throw new IllegalArgumentException("Discount must be between 0 and 1."); + } + return items.stream() + .map(item -> new ItemPrice(item.itemId(), item.price() * (1 - discountPercentage))) + .collect(Collectors.toUnmodifiableList()); // Returns a new, immutable list + } +} + +class PureFunctionExample { + public static void main(String args) { + double itemPrice = 100.0; + double tax = 0.07; + double total = PriceOperations.calculateTotalWithTax(itemPrice, tax); + System.out.println("Total for price " + itemPrice + " with tax " + tax + ": " + total); + + List catalogue = List.of( + new ItemPrice("A001", 50.0), + new ItemPrice("B002", 120.0) + ); + List discountedCatalogue = PriceOperations.applyDiscountToItems(catalogue, 0.10); // 10% discount + System.out.println("Original catalogue: " + catalogue); + System.out.println("Discounted catalogue: " + discountedCatalogue); + } +} +``` + +**Bad example:** + +```java +import java.util.List; +import java.util.ArrayList; + +// Impure function with side effects and dependency on instance state +class PriceCalculator { + private double taxRate; // Instance state + private List prices; // Modifies this list + + public PriceCalculator(double taxRate, List initialPrices) { + this.taxRate = taxRate; + this.prices = new ArrayList<>(initialPrices); // Stores mutable state + } + + // Depends on instance state (taxRate) + public double calculateTotalForItem(double price) { + return price * (1 + this.taxRate); + } + + // Modifies instance state (this.prices) - a side effect + public void applyDiscountToAllItems(double discount) { + for (int i = 0; i < this.prices.size(); i++) { + this.prices.set(i, this.prices.get(i) * (1 - discount)); + } + } + + public List getPrices() { + return this.prices; + } + + public static void main(String args) { + PriceCalculator calc = new PriceCalculator(0.05, List.of(10.0, 20.0)); + System.out.println("Total for 10.0: " + calc.calculateTotalForItem(10.0)); // Depends on calc.taxRate + + System.out.println("Prices before discount: " + calc.getPrices()); + calc.applyDiscountToAllItems(0.1); + System.out.println("Prices after discount (mutated): " + calc.getPrices()); // Original list inside calc is modified + } +} +``` + +## Rule 4: Keep Data Flat and Denormalized + +Title: Prefer Flatter Data Structures with References +Description: - Avoid excessively deep nesting of data objects, which can make data harder to access and manage. - When representing relationships, consider using references (like IDs) instead of directly embedding complex objects within other objects, especially for many-to-many or one-to-many relationships. - Store related but distinct entities in separate flat collections or maps, linked by these IDs. - This approach can simplify querying, updating, and serializing data. - However, balance this with the need for data co-location for performance in specific access patterns. Denormalization is a trade-off. + +**Good example:** + +```java +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.stream.Collectors; +import java.util.Objects; + +// Flat structures with references (IDs) +record TaskData(String id, String name, String description, String assigneeId) {} +record ProjectData(String id, String name, List taskIds) {} +record EmployeeData(String id, String name, String departmentId, List projectIds) {} +record DepartmentData(String id, String name, List employeeIds) {} + +class FlatDataStore { + private final Map tasks = new HashMap<>(); + private final Map projects = new HashMap<>(); + private final Map employees = new HashMap<>(); + private final Map departments = new HashMap<>(); + + // Methods to add and retrieve data (simplified) + public void addTask(TaskData task) { tasks.put(task.id(), task); } + public TaskData getTask(String id) { return tasks.get(id); } + // ... similar methods for projects, employees, departments + + public List getTasksForProject(String projectId) { + ProjectData project = projects.get(projectId); + if (Objects.isNull(project)) return List.of(); + return project.taskIds().stream() + .map(tasks::get) + .collect(Collectors.toList()); + } + public static void main(String args) { + FlatDataStore store = new FlatDataStore(); + store.addTask(new TaskData("T1", "Design UI", "...", "E1")); + // ... add more data + // Accessing data involves looking up by ID and potentially joining. + } +} +``` + +**Bad example:** + +```java +import java.util.List; +import java.util.ArrayList; + +// Deeply nested structure +class Department { + private String departmentName; + private List employees; + public Department(String name) { this.departmentName = name; this.employees = new ArrayList<>(); } + public void addEmployee(Employee e) { this.employees.add(e); } + public List getEmployees() { return employees; } +} + +class Employee { + private String employeeName; + private List projects; + public Employee(String name) { this.employeeName = name; this.projects = new ArrayList<>(); } + public void addProject(Project p) { this.projects.add(p); } + public List getProjects() { return projects; } +} + +class Project { + private String projectName; + private List tasks; + public Project(String name) { this.projectName = name; this.tasks = new ArrayList<>(); } + public void addTask(Task t) { this.tasks.add(t); } + public List getTasks() { return tasks; } +} + +class Task { + private String taskName; + public Task(String name) { this.taskName = name; } + public String getTaskName() { return taskName; } +} + +class DeeplyNestedExample { + public static void main(String args) { + Department dept = new Department("Engineering"); + Employee emp = new Employee("Alice"); + Project proj = new Project("New Platform"); + Task task1 = new Task("Define API"); + + proj.addTask(task1); + emp.addProject(proj); + dept.addEmployee(emp); + // Accessing task1 requires: dept.getEmployees().get(0).getProjects().get(0).getTasks().get(0) + // Updates are complex. Serialization can be very large. + System.out.println(dept.getEmployees().get(0).getProjects().get(0).getTasks().get(0).getTaskName()); + } +} +``` + +## Rule 5: Keep Data Generic Until Specific + +Title: Start with Generic Data Structures, Convert to Specific Types When Needed +Description: - For highly dynamic or externally defined data, start with generic data structures like `Map` or JSON-like trees. - This allows flexibility in handling varied or evolving data schemas. - Convert to specific, strongly-typed objects (like records) only when the data needs to be processed in a type-safe manner or when specific business logic applies. - Implement robust, type-safe converter functions or classes to perform this transformation. - Validate data during the conversion process to ensure it conforms to the expected specific type. - Clearly document the expected structure of the generic data. + +**Good example:** + +```java +import java.util.Map; +import java.util.HashMap; +import java.time.LocalDate; +import java.util.Objects; + +// Specific target type (record) +record UserProfile( + String email, + String firstName, + String lastName, + LocalDate birthDate +) {} + +// Generic data carrier (could be a simple Map or a wrapper record) +record GenericData(Map attributes) {} + +// Converter class +class UserProfileConverter { + public static UserProfile toUserProfile(GenericData genericData) { + Map attrs = genericData.attributes(); + + // Basic validation and type casting (could be more robust) + String email = (String) attrs.get("email"); + String firstName = (String) attrs.get("firstName"); + String lastName = (String) attrs.get("lastName"); + LocalDate birthDate = null; + Object bd = attrs.get("birthDate"); + if (bd instanceof String) { + birthDate = LocalDate.parse((String) bd); + } else if (bd instanceof LocalDate) { + birthDate = (LocalDate) bd; + } else if (bd != null) { + throw new IllegalArgumentException("Invalid birthDate format"); + } + + if (Objects.isNull(email) || Objects.isNull(firstName) || Objects.isNull(lastName) || Objects.isNull(birthDate)) { + throw new IllegalArgumentException("Missing required user profile fields"); + } + + return new UserProfile(email, firstName, lastName, birthDate); + } + + public static GenericData fromUserProfile(UserProfile userProfile) { + Map attrs = new HashMap<>(); + attrs.put("email", userProfile.email()); + attrs.put("firstName", userProfile.firstName()); + attrs.put("lastName", userProfile.lastName()); + attrs.put("birthDate", userProfile.birthDate().toString()); + return new GenericData(attrs); + } +} + +class GenericDataExample { + public static void main(String args) { + Map rawData = new HashMap<>(); + rawData.put("email", "alice@example.com"); + rawData.put("firstName", "Alice"); + rawData.put("lastName", "Wonder"); + rawData.put("birthDate", "1990-07-15"); + rawData.put("extraField", "someValue"); // Generic data can have extra fields + + GenericData genericUser = new GenericData(rawData); + System.out.println("Generic user data: " + genericUser.attributes()); + + try { + UserProfile specificProfile = UserProfileConverter.toUserProfile(genericUser); + System.out.println("Converted to specific UserProfile: " + specificProfile); + + GenericData backToGeneric = UserProfileConverter.fromUserProfile(specificProfile); + System.out.println("Converted back to generic: " + backToGeneric.attributes()); + + } catch (IllegalArgumentException e) { + System.err.println("Conversion error: " + e.getMessage()); + } + } +} +``` + +**Bad example:** + +```java +import java.time.LocalDate; + +// Too specific too early, making it rigid if data source changes slightly +record UserProfileRigid( + String email, + String password, // If password isn't always present or needed for all uses + String firstName, + String lastName, + LocalDate birthDate + // What if new optional fields are added by the source? This class needs constant updates. +) { + // This class is fine if the data is *always* in this exact shape and used this way, + // but not flexible for varying inputs. +} + +class RigidDataExample { + public static void main(String args) { + // If data comes from an external source that might omit 'password' or add new fields, + // directly deserializing into UserProfileRigid might fail or ignore data. + // UserProfileRigid profile = new UserProfileRigid(...); + System.out.println("This example illustrates inflexibility if source data structure is volatile."); + } +} +``` + +## Rule 6: Data Integrity through Validation Functions + +Title: Ensure Data Integrity with Pure Validation Functions +Description: - Implement data validation logic as pure functions that take data as input and return validation results (e.g., a list of errors, or an object indicating success/failure). - These functions should not throw exceptions for validation failures as a primary flow control; instead, they should return information about the validation outcome. - Multiple validation rules can be composed or chained together. - Consider using `Optional` or a custom result type to represent validation messages for individual rules. - Keep validation logic separate from the data structures themselves. - Make validation rules reusable across different parts of the application. + +**Good example:** + +```java +import java.util.List; +import java.util.ArrayList; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.Objects; + +record UserRecord(String id, String email, int age) {} + +// Validation functions as static methods in a utility class +class UserValidators { + private static final Pattern EMAIL_PATTERN = Pattern.compile("^\\w.-+@\\w.-+\\.a-zA-Z{2,}$"); + + public static Optional validateEmailFormat(String email) { + if (Objects.isNull(email) || !EMAIL_PATTERN.matcher(email).matches()) { + return Optional.of("Invalid email format for: " + email); + } + return Optional.empty(); // No error + } + + public static Optional validateAgeRange(int age) { + if (age < 0 || age > 150) { + return Optional.of("Age must be between 0 and 150, but was: " + age); + } + return Optional.empty(); // No error + } + + // Composite validation for a UserRecord + public static List validateUser(UserRecord user) { + return Stream.of( + validateEmailFormat(user.email()), + validateAgeRange(user.age()) + ) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } +} + +class ValidationFunctionExample { + public static void main(String args) { + UserRecord user1 = new UserRecord("u1", "alice@example.com", 30); + List errors1 = UserValidators.validateUser(user1); + if (errors1.isEmpty()) { + System.out.println("User 1 is valid: " + user1); + } else { + System.out.println("User 1 validation errors: " + errors1); + } + + UserRecord user2 = new UserRecord("u2", "bob.com", -5); + List errors2 = UserValidators.validateUser(user2); + if (errors2.isEmpty()) { + System.out.println("User 2 is valid: " + user2); + } else { + System.out.println("User 2 validation errors: " + errors2); + } + } +} +``` + +**Bad example:** + +```java +// Validation logic mixed into data object, or throwing exceptions for normal flow +import java.util.Objects; +record UserWithEmbeddedValidation(String email, int age) { + public UserWithEmbeddedValidation { + // Validation in constructor - can be problematic + if (Objects.isNull(email) || !email.contains("@")) { + throw new IllegalArgumentException("Invalid email in constructor"); + } + if (age < 0) { + throw new IllegalArgumentException("Age cannot be negative in constructor"); + } + } + + // Or validation method that throws exceptions + public void validate() { + if (Objects.isNull(email) || !email.contains("@")) { + throw new IllegalStateException("User email is invalid"); + } + if (age < 0) { + throw new IllegalStateException("User age is invalid"); + } + System.out.println("User is valid by throwing method."); + } + public static void main(String args) { + try { + UserWithEmbeddedValidation user = new UserWithEmbeddedValidation("test", -1); + user.validate(); + } catch (IllegalArgumentException | IllegalStateException e) { + System.err.println("Validation failed: " + e.getMessage()); + // Using exceptions for validation flow control is generally an anti-pattern. + } + } +} +``` + +## Rule 7: Flexible and Generic Data Access + +Title: Design Flexible and Generic Data Access Layers +Description: - Define generic interfaces for data access operations (CRUD - Create, Read, Update, Delete). - Use type parameters (``) in these interfaces to allow them to work with various data types. - Implementations of these interfaces can then provide data storage and retrieval using different mechanisms (e.g., in-memory maps, databases, file systems) while the calling code remains decoupled. - Ensure data storage implementations are thread-safe if they will be accessed concurrently. - Support common querying needs like filtering or finding by ID. - Design in a way that allows the underlying storage implementation to be easily swapped or replaced. - Consider caching strategies at this layer for performance improvement. + +**Good example:** + +```java +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +// Generic data access interface +interface DataStore { // Added ID type parameter + Optional findById(ID id); + List findAll(); + List findByPredicate(Predicate filter); + void save(ID id, T data); + void deleteById(ID id); + // Potentially add update methods, etc. +} + +// Example record to store +record Product(String id, String name, double price) {} + +// In-memory implementation using generic data structures +class InMemoryDataStore implements DataStore { + private final Map storage = new ConcurrentHashMap<>(); // Thread-safe for concurrent access + + @Override + public Optional findById(ID id) { + return Optional.ofNullable(storage.get(id)); + } + + @Override + public List findAll() { + return List.copyOf(storage.values()); // Return immutable copy + } + + @Override + public List findByPredicate(Predicate filter) { + return storage.values().stream() + .filter(filter) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public void save(ID id, T data) { + storage.put(id, data); + System.out.println("Saved: " + data); + } + + @Override + public void deleteById(ID id) { + storage.remove(id); + System.out.println("Deleted item with id: " + id); + } +} + +class DataAccessExample { + public static void main(String args) { + DataStore productStore = new InMemoryDataStore<>(); + + Product laptop = new Product("P101", "Laptop Pro", 1200.00); + Product mouse = new Product("P102", "Wireless Mouse", 25.00); + + productStore.save(laptop.id(), laptop); + productStore.save(mouse.id(), mouse); + + System.out.println("Find P101: " + productStore.findById("P101")); + System.out.println("All products: " + productStore.findAll()); + System.out.println("Expensive products: " + + productStore.findByPredicate(p -> p.price() > 100.0)); + + productStore.deleteById("P102"); + System.out.println("All products after delete: " + productStore.findAll()); + } +} +``` + +**Bad example:** + +```java +import java.util.HashMap; +import java.util.Map; + +// Non-generic, tightly coupled data access for a specific type +class SpecificProductDao { + // Not thread-safe if used concurrently from multiple threads + private final Map productTable = new HashMap<>(); + + public Product getProductById(String id) { + return productTable.get(id); // Returns null if not found, no Optional + } + + public void addProduct(Product product) { + // If Product had no ID field, keying would be arbitrary + productTable.put(product.id(), product); + } + // No generic way to handle other types. + // If we need a User DAO, we write a whole new class similar to this. + // Difficult to swap storage implementation. +} + +class BadDataAccessExample { + public static void main(String args) { + // (Using Product record from Good Example) + SpecificProductDao dao = new SpecificProductDao(); + dao.addProduct(new Product("P001", "Old Monitor", 150.0)); + System.out.println("Product P001: " + dao.getProductById("P001")); + // Problems: not generic, not easily testable with mocks if directly instantiated, + // not thread-safe by default, harder to maintain for many data types. + } +} +``` + +## Rule 8: Explicit and Traceable Data Transformation + +Title: Ensure Data Transformations are Explicit and Traceable +Description: - Make all data transformation steps visible and easy to follow. - Use sequences of pure functions for complex transformations, making each step clear. - Avoid hidden or implicit transformations within objects or complex method calls. - Consider logging input, output, and intermediate steps of significant transformations, especially during debugging or for auditing. - Handle potential errors or exceptional cases gracefully within the transformation pipeline. - Document the overall flow and purpose of complex data transformations. + +**Good example:** + +```java +import java.util.Map; +import java.util.function.Function; + +// Source data record +record RawOrder(String orderId, String customerId, double amount, String currency, Map items) {} + +// Target DTOs +record ProcessedOrderHeader(String orderId, String customerId, double normalizedAmount) {} +record OrderItem(String itemId, int quantity) {} // Assuming items map needs parsing + +class OrderProcessingService { + + // Transformation Step 1: Normalize currency (pure function) + private static Function normalizeCurrency(String targetCurrency, Map rates) { + return rawOrder -> { + if (rawOrder.currency().equals(targetCurrency)) { + return rawOrder; + } + double rate = rates.getOrDefault(rawOrder.currency(), 1.0); // Default to 1.0 if rate not found + double newAmount = rawOrder.amount() * rate; + System.out.println("Trace Normalizing currency for order " + rawOrder.orderId() + ": " + rawOrder.amount() + " " + rawOrder.currency() + " -> " + newAmount + " " + targetCurrency); + return new RawOrder(rawOrder.orderId(), rawOrder.customerId(), newAmount, targetCurrency, rawOrder.items()); + }; + } + + // Transformation Step 2: Extract header (pure function) + private static Function extractHeader() { + return rawOrder -> { + System.out.println("Trace Extracting header for order " + rawOrder.orderId()); + return new ProcessedOrderHeader(rawOrder.orderId(), rawOrder.customerId(), rawOrder.amount()); + }; + } + + // Overall transformation pipeline + public static ProcessedOrderHeader processOrder(RawOrder rawOrder, String targetCurrency, Map exchangeRates) { + System.out.println("Trace Starting processing for order: " + rawOrder.orderId()); + + ProcessedOrderHeader header = normalizeCurrency(targetCurrency, exchangeRates) + .andThen(extractHeader()) // Chaining transformations + .apply(rawOrder); + + System.out.println("Trace Finished processing. Header: " + header); + return header; + // Further steps could parse items, validate, etc. + } +} + +class TransformationTraceExample { + public static void main(String args) { + RawOrder order1 = new RawOrder("ORD123", "CUST001", 100.0, "USD", Map.of("itemA", "2")); + RawOrder order2 = new RawOrder("ORD456", "CUST002", 85.0, "EUR", Map.of("itemB", "1")); + + Map ratesToUSD = Map.of("EUR", 1.08, "USD", 1.0); + + ProcessedOrderHeader processed1 = OrderProcessingService.processOrder(order1, "USD", ratesToUSD); + System.out.println("Processed Order 1: " + processed1); + + ProcessedOrderHeader processed2 = OrderProcessingService.processOrder(order2, "USD", ratesToUSD); + System.out.println("Processed Order 2: " + processed2); + } +} +``` + +**Bad example:** + +```java +import java.util.Map; + +// Complex object that mutates itself and has hidden transformations +class MutableOrder { + public String orderId; + public double amount; + public String currency; + // ... other fields + + public MutableOrder(String orderId, double amount, String currency) { + this.orderId = orderId; + this.amount = amount; + this.currency = currency; + } + + // Hidden transformation, mutates state, depends on external static rates + public void convertToCurrency(String targetCurrency) { + // Imagine static ExchangeRateProvider.getRate(this.currency, targetCurrency) + // This is a side effect, modifies the object in place, hard to trace. + if (!this.currency.equals(targetCurrency)) { + System.out.println("Converting order " + orderId + " to " + targetCurrency + " (details hidden)"); + // this.amount = this.amount * ExchangeRateProvider.getRate(this.currency, targetCurrency); + // this.currency = targetCurrency; + // Forcing an example without actual external call + if (this.currency.equals("EUR") && targetCurrency.equals("USD")) { + this.amount = this.amount * 1.08; + this.currency = targetCurrency; + } + } + } + @Override public String toString() { return "MutableOrder{id='"+orderId+"', amount="+amount+", currency='"+currency+"'}";} +} + +class HiddenTransformationExample { + public static void main(String args) { + MutableOrder order = new MutableOrder("ORD789", 50.0, "EUR"); + System.out.println("Original order: " + order); + + // Call a method that internally and perhaps opaquely transforms the order + order.convertToCurrency("USD"); + System.out.println("Transformed order: " + order); + // It's not clear from the call site what exactly changed or how. + // If multiple such methods are called, the state becomes hard to reason about. + } +} +``` \ No newline at end of file