Class JoomlaApplicationSpec

java.lang.Object
com.cloudforgeci.api.application.cms.JoomlaApplicationSpec
All Implemented Interfaces:
ApplicationSpec, CmsSpec, DatabaseSpec

@CmsPlugin(value="joomla", category="cms", displayName="Joomla", description="Flexible CMS for websites and web applications", phpVersion="8.2", defaultCpu=1024, defaultMemory=2048, defaultInstanceType="t3.small", supportsOidc=true, oidcMethod="miniOrange OIDC Plugin", requiresDatabase=true, supportedDatabases={"mysql","mariadb","postgresql"}, supportsS3Media=true, supportsObjectCache=true, supportsMultisite=false, websiteUrl="https://www.joomla.org", defaultImage="joomla:5-php8.2-apache") public class JoomlaApplicationSpec extends Object implements CmsSpec, DatabaseSpec
Joomla CMS ApplicationSpec implementation.

Joomla is a flexible, open-source CMS used for building websites and web applications. It has a large extension ecosystem and is popular for corporate websites, portals, and community sites.

Key Features:

  • PHP 8.2 with standard extensions
  • MySQL/MariaDB/PostgreSQL database support
  • Built-in multilingual support
  • Extensive template system
  • Large extension marketplace
Since:
3.1.0
See Also:
  • Field Details

  • Constructor Details

    • JoomlaApplicationSpec

      public JoomlaApplicationSpec()
  • Method Details

    • applicationId

      public String applicationId()
      Description copied from interface: ApplicationSpec
      Returns a unique identifier for this application. Used for logging, metrics, and resource naming.
      Specified by:
      applicationId in interface ApplicationSpec
      Returns:
      application identifier (e.g., "jenkins", "gitlab", "vault")
    • defaultContainerImage

      public String defaultContainerImage()
      Description copied from interface: ApplicationSpec
      Returns the default container image for this application. Can be overridden by deployment context configuration.
      Specified by:
      defaultContainerImage in interface ApplicationSpec
      Returns:
      container image string (e.g., "jenkins/jenkins:lts")
    • applicationPort

      public int applicationPort()
      Description copied from interface: ApplicationSpec
      Returns the primary application port. This is the port the application listens on inside the container.
      Specified by:
      applicationPort in interface ApplicationSpec
      Returns:
      application port (e.g., 8080 for Jenkins)
    • containerDataPath

      public String containerDataPath()
      Description copied from interface: ApplicationSpec
      Returns the container path where application data is stored. This is where the volume will be mounted inside the container.
      Specified by:
      containerDataPath in interface ApplicationSpec
      Returns:
      container mount path (e.g., "/var/jenkins_home")
    • efsDataPath

      public String efsDataPath()
      Description copied from interface: ApplicationSpec
      Returns the EFS path for this application's data. This is the path within the EFS filesystem.
      Specified by:
      efsDataPath in interface ApplicationSpec
      Returns:
      EFS path (e.g., "/jenkins")
    • volumeName

      public String volumeName()
      Description copied from interface: ApplicationSpec
      Returns the volume name for this application. Used to reference the volume in task definitions.
      Specified by:
      volumeName in interface ApplicationSpec
      Returns:
      volume name (e.g., "jenkinsHome")
    • containerUser

      public String containerUser()
      Description copied from interface: ApplicationSpec
      Returns the container user (UID:GID) to run as. Important for file permissions when using EFS.
      Specified by:
      containerUser in interface ApplicationSpec
      Returns:
      user in format "UID:GID" (e.g., "1000:1000")
    • efsPermissions

      public String efsPermissions()
      Description copied from interface: ApplicationSpec
      Returns the EFS permissions for the access point.
      Specified by:
      efsPermissions in interface ApplicationSpec
      Returns:
      permissions string (e.g., "750")
    • healthCheckPath

      public String healthCheckPath()
      Description copied from interface: ApplicationSpec
      Returns the health check path for ALB/ELB health checks.

      Different applications expose health endpoints at different paths:

      • Jenkins: /login
      • GitLab: /users/sign_in
      • Grafana: /api/health
      • Metabase: /api/health
      Specified by:
      healthCheckPath in interface ApplicationSpec
      Returns:
      health check path (e.g., "/login", "/api/health")
    • defaultHealthCheckGracePeriod

      public int defaultHealthCheckGracePeriod()
      Description copied from interface: ApplicationSpec
      Get the recommended health check grace period for this application.

      The grace period is how long ECS/ALB waits before starting health checks after a container starts. Applications with longer initialization times (like GitLab) need longer grace periods.

      Default values:

      • Most applications: 300 seconds (5 minutes)
      • GitLab: 600 seconds (10 minutes) - due to database migrations and initialization
      • Other database-heavy apps may also need longer periods
      Specified by:
      defaultHealthCheckGracePeriod in interface ApplicationSpec
      Returns:
      recommended health check grace period in seconds
    • phpVersion

      public String phpVersion()
      Description copied from interface: CmsSpec
      Returns the required PHP version for this CMS.

      Common versions:

      • 8.2 - WordPress, Magento 2.4.6+, Drupal 10
      • 8.1 - PrestaShop 8.x, Joomla 5
      • 7.4 - Legacy support (not recommended)
      Specified by:
      phpVersion in interface CmsSpec
      Returns:
      PHP version string (e.g., "8.2", "8.1")
    • requiredPhpExtensions

      public List<String> requiredPhpExtensions()
      Description copied from interface: CmsSpec
      Returns required PHP extensions for this CMS.

      Common extensions include:

      • mysqli, pdo_mysql - MySQL database connectivity
      • gd, imagick - Image processing
      • curl - HTTP client
      • mbstring - Multibyte string handling
      • xml, dom - XML processing
      • zip - Archive handling
      • intl - Internationalization
      • opcache - Bytecode caching
      • redis - Redis client
      Specified by:
      requiredPhpExtensions in interface CmsSpec
      Returns:
      List of PHP extension names
    • phpFpmConfig

      public Map<String,String> phpFpmConfig()
      Description copied from interface: CmsSpec
      Returns PHP-FPM pool configuration overrides.

      Common settings:

      • pm - Process manager (static, dynamic, ondemand)
      • pm.max_children - Maximum worker processes
      • pm.start_servers - Initial workers (dynamic mode)
      • pm.min_spare_servers - Minimum idle workers
      • pm.max_spare_servers - Maximum idle workers
      • pm.max_requests - Requests before worker recycle
      Specified by:
      phpFpmConfig in interface CmsSpec
      Returns:
      Map of PHP-FPM configuration key-value pairs
    • opcacheConfig

      public Map<String,String> opcacheConfig()
      Description copied from interface: CmsSpec
      Returns OPcache configuration for PHP bytecode caching.

      Recommended production settings:

      • opcache.enable=1 - Enable OPcache
      • opcache.memory_consumption=128 - Cache memory (MB)
      • opcache.max_accelerated_files=10000 - Cached file limit
      • opcache.revalidate_freq=60 - File check interval (seconds)
      • opcache.validate_timestamps=0 - Disable for production
      Specified by:
      opcacheConfig in interface CmsSpec
      Returns:
      Map of OPcache configuration key-value pairs
    • phpMemoryLimit

      public int phpMemoryLimit()
      Description copied from interface: CmsSpec
      Returns PHP memory limit in megabytes.

      Recommended values:

      • WordPress: 256MB
      • WooCommerce: 512MB
      • Magento: 756MB-2GB
      • Drupal: 256MB
      Specified by:
      phpMemoryLimit in interface CmsSpec
      Returns:
      Memory limit in MB
    • phpMaxExecutionTime

      public int phpMaxExecutionTime()
      Description copied from interface: CmsSpec
      Returns PHP max execution time in seconds.
      Specified by:
      phpMaxExecutionTime in interface CmsSpec
      Returns:
      Max execution time (default: 300 seconds)
    • phpUploadMaxFilesize

      public int phpUploadMaxFilesize()
      Description copied from interface: CmsSpec
      Returns PHP upload max filesize in megabytes.
      Specified by:
      phpUploadMaxFilesize in interface CmsSpec
      Returns:
      Upload max filesize in MB (default: 64MB)
    • phpPostMaxSize

      public int phpPostMaxSize()
      Description copied from interface: CmsSpec
      Returns PHP post max size in megabytes.
      Specified by:
      phpPostMaxSize in interface CmsSpec
      Returns:
      Post max size in MB (default: 64MB)
    • supportsS3MediaStorage

      public boolean supportsS3MediaStorage()
      Description copied from interface: CmsSpec
      Returns whether S3 media offloading is supported.

      When enabled, media uploads are stored in S3 instead of local filesystem, enabling horizontal scaling and CDN integration.

      Specified by:
      supportsS3MediaStorage in interface CmsSpec
      Returns:
      true if S3 media storage is supported
    • s3MediaPlugin

      public String s3MediaPlugin()
      Description copied from interface: CmsSpec
      Returns the plugin/module identifier for S3 media integration.

      Examples:

      • WordPress: "wp-offload-media" or "amazon-s3-and-cloudfront"
      • Magento: "magento/module-aws-s3"
      • Drupal: "s3fs"
      Specified by:
      s3MediaPlugin in interface CmsSpec
      Returns:
      Plugin identifier, or null if native S3 support
    • mediaUploadPath

      public String mediaUploadPath()
      Description copied from interface: CmsSpec
      Returns the local media upload path within the container.

      Examples:

      • WordPress: "/var/www/html/wp-content/uploads"
      • Magento: "/var/www/html/pub/media"
      • Drupal: "/var/www/html/sites/default/files"
      Specified by:
      mediaUploadPath in interface CmsSpec
      Returns:
      Absolute path to media upload directory
    • supportsCdnIntegration

      public boolean supportsCdnIntegration()
      Description copied from interface: CmsSpec
      Returns whether CDN integration is supported.

      CDN integration via CloudFront enables:

      • Edge caching for static assets
      • Global content delivery
      • SSL termination at edge
      • DDoS protection
      Specified by:
      supportsCdnIntegration in interface CmsSpec
      Returns:
      true if CDN integration is supported (default: true)
    • cdnMediaPaths

      public List<String> cdnMediaPaths()
      Description copied from interface: CmsSpec
      Returns paths served from the S3 media origin in CloudFront.

      These paths correspond to user-uploaded media files (images, videos, documents) that are offloaded to S3. They will be routed to the S3 origin with a long-TTL media cache policy.

      Examples:

      • WordPress: ["/wp-content/uploads/*"]
      • Magento: ["/media/*"]
      • Drupal: ["/sites/default/files/*"]
      Specified by:
      cdnMediaPaths in interface CmsSpec
      Returns:
      List of URL path patterns for S3 media origin (default: derived from CmsSpec.mediaUploadPath(), empty if that path isn't under the web root)
    • cdnStaticPaths

      public List<String> cdnStaticPaths()
      Description copied from interface: CmsSpec
      Returns static asset paths cached at the edge in CloudFront.

      These paths (CSS, JS, fonts, theme assets) are served from the ALB origin with a long-TTL static cache policy. They do not include user-uploaded media; see CmsSpec.cdnMediaPaths() for that.

      Examples:

      • WordPress: ["/wp-content/themes/*", "/wp-content/plugins/*", "/wp-includes/*"]
      • Magento: ["/static/*"]
      • Drupal: ["/core/*", "/modules/*", "/themes/*"]
      Specified by:
      cdnStaticPaths in interface CmsSpec
      Returns:
      List of URL path patterns for edge-cached static assets (default: empty)
    • cdnAdminPaths

      public List<String> cdnAdminPaths()
      Description copied from interface: CmsSpec
      Returns admin/back-office paths that bypass CDN caching entirely.

      Requests to these paths are forwarded to the ALB with caching disabled and all headers/cookies forwarded, ensuring the CMS admin panel receives full session state.

      This is also the single source of truth for ALB OIDC path-based authentication. The default CmsSpec.protectedPaths() implementation below delegates here, so CMS implementations only need to override cdnAdminPaths() and both CDN routing and ALB auth stay in sync.

      Examples:

      • WordPress: ["/wp-admin/*", "/wp-login.php"]
      • Magento: ["/admin/*", "/backend/*"]
      • Joomla: ["/administrator/*"]
      Specified by:
      cdnAdminPaths in interface CmsSpec
      Returns:
      List of URL path patterns that must not be cached (default: empty)
    • redisEnvVars

      public Map<String,String> redisEnvVars(String host, int port)
      Description copied from interface: CmsSpec
      Returns Redis connection environment variables for this CMS.

      The default implementation returns the generic REDIS_HOST / REDIS_PORT pair. CMS implementations should override to add their plugin-specific variable names.

      Examples of CMS-specific additions:

      • WordPress: WP_REDIS_HOST, WP_REDIS_PORT, WP_REDIS_DATABASE
      • Magento: MAGENTO_CACHE_BACKEND_REDIS_SERVER (+ page-cache and session variants)
      • Drupal: DRUPAL_REDIS_HOST, DRUPAL_REDIS_PORT
      Specified by:
      redisEnvVars in interface CmsSpec
      Parameters:
      host - Redis primary endpoint hostname
      port - Redis port (usually 6379)
      Returns:
      mutable map of environment variable key-value pairs
    • databaseEnvVars

      public Map<String,String> databaseEnvVars(String host, int port, String name, String user)
      Description copied from interface: CmsSpec
      Returns database connection environment variables for this CMS.

      The default implementation returns generic DB_HOST / DB_PORT / DB_NAME / DB_USER. CMS implementations should override to add their native variable names.

      Examples of CMS-specific additions:

      • WordPress: WORDPRESS_DB_HOST, WORDPRESS_DB_NAME, WORDPRESS_DB_USER
      • Magento: MAGENTO_DATABASE_HOST, MAGENTO_DATABASE_NAME
      • Drupal: DRUPAL_DATABASE_HOST, DRUPAL_DATABASE_DRIVER
      Specified by:
      databaseEnvVars in interface CmsSpec
      Parameters:
      host - RDS endpoint hostname
      port - database port (3306 for MySQL/MariaDB, 5432 for Postgres)
      name - database name
      user - database username
      Returns:
      mutable map of environment variable key-value pairs
    • supportsObjectCache

      public boolean supportsObjectCache()
      Description copied from interface: CmsSpec
      Returns whether Redis/Memcached object caching is supported.

      Object caching stores database query results and computed values in memory for faster retrieval.

      Specified by:
      supportsObjectCache in interface CmsSpec
      Returns:
      true if object caching is supported
    • preferredCacheBackend

      public String preferredCacheBackend()
      Description copied from interface: CmsSpec
      Returns the preferred caching backend.
      Specified by:
      preferredCacheBackend in interface CmsSpec
      Returns:
      "redis", "memcached", or "none"
    • objectCachePlugin

      public String objectCachePlugin()
      Description copied from interface: CmsSpec
      Returns the object cache plugin/module identifier.

      Examples:

      • WordPress: "redis-cache" (Redis Object Cache plugin)
      • Magento: Built-in Redis support
      • Drupal: "redis" module
      Specified by:
      objectCachePlugin in interface CmsSpec
      Returns:
      Plugin identifier for object caching
    • hasScheduledTasks

      public boolean hasScheduledTasks()
      Description copied from interface: CmsSpec
      Returns whether the CMS has scheduled tasks (cron jobs).

      Most CMS platforms have internal task schedulers:

      • WordPress: WP-Cron
      • Magento: Cron groups (index, default, consumers)
      • Drupal: Cron module
      Specified by:
      hasScheduledTasks in interface CmsSpec
      Returns:
      true if scheduled tasks exist
    • useSystemCron

      public boolean useSystemCron()
      Description copied from interface: CmsSpec
      Returns whether to use system cron instead of internal scheduler.

      System cron is recommended for production because:

      • More reliable execution timing
      • Reduced page load overhead
      • Better control over resource usage
      Specified by:
      useSystemCron in interface CmsSpec
      Returns:
      true to disable internal cron and use system cron
    • cronCommands

      public Map<String,String> cronCommands(String siteUrl)
      Description copied from interface: CmsSpec
      Returns scheduled task commands for system cron.

      Map keys are cron schedule expressions, values are commands.

      Example for WordPress:

      "* /15 * * * *" -> "curl -s https://example.com/wp-cron.php"
      
      Specified by:
      cronCommands in interface CmsSpec
      Parameters:
      siteUrl - The site URL for cron execution
      Returns:
      Map of cron schedule to command
    • supportsMultisite

      public boolean supportsMultisite()
      Description copied from interface: CmsSpec
      Returns whether multi-site/multi-store is supported.

      Multi-site capabilities:

      • WordPress: Multisite network
      • Magento: Multi-store views
      • Drupal: Multi-site configuration
      • PrestaShop: Multi-shop
      Specified by:
      supportsMultisite in interface CmsSpec
      Returns:
      true if multi-site is available
    • multisiteMode

      public String multisiteMode()
      Description copied from interface: CmsSpec
      Returns the multi-site configuration mode.

      Modes:

      • "subdomain" - sites.example.com
      • "subdirectory" - example.com/sites/
      • "domain" - separate domains per site
      • "none" - multi-site not enabled
      Specified by:
      multisiteMode in interface CmsSpec
      Returns:
      Multi-site mode string
    • cmsCategory

      public String cmsCategory()
      Description copied from interface: CmsSpec
      Returns the CMS category.

      Categories:

      • "cms" - Content management (WordPress, Joomla, Drupal)
      • "ecommerce" - E-commerce (WooCommerce, Magento, PrestaShop)
      Specified by:
      cmsCategory in interface CmsSpec
      Returns:
      "cms" or "ecommerce"
    • preferredWebServer

      public String preferredWebServer()
      Description copied from interface: CmsSpec
      Returns the preferred web server.
      Specified by:
      preferredWebServer in interface CmsSpec
      Returns:
      "nginx", "apache", or "caddy"
    • documentRoot

      public String documentRoot()
      Description copied from interface: CmsSpec
      Returns the document root path within the container.

      This is where the web server serves files from.

      Specified by:
      documentRoot in interface CmsSpec
      Returns:
      Document root path (e.g., "/var/www/html")
    • cliTool

      public String cliTool()
      Description copied from interface: CmsSpec
      Returns the CLI tool for this CMS, if available.

      Examples:

      • WordPress: "wp" (WP-CLI)
      • Magento: "bin/magento"
      • Drupal: "drush"
      • Joomla: "cli/joomla.php"
      Specified by:
      cliTool in interface CmsSpec
      Returns:
      CLI tool command, or null if not available
    • cliToolInstallCommands

      public List<String> cliToolInstallCommands()
      Description copied from interface: CmsSpec
      Returns commands to install the CLI tool.
      Specified by:
      cliToolInstallCommands in interface CmsSpec
      Returns:
      List of shell commands to install CLI tool
    • databaseRequirement

      public DatabaseSpec.DatabaseRequirement databaseRequirement()
      Description copied from interface: DatabaseSpec
      Database requirement for this application.
      Specified by:
      databaseRequirement in interface DatabaseSpec
      Returns:
      database requirement (required, optional, or none)
    • backupRetentionDays

      public int backupRetentionDays()
      Description copied from interface: DatabaseSpec
      Database backup retention requirements.
      Specified by:
      backupRetentionDays in interface DatabaseSpec
      Returns:
      backup retention days (1-35), default 7
    • ebsDeviceName

      public String ebsDeviceName()
      Description copied from interface: ApplicationSpec
      Returns the EBS device name for EC2 instances when not using EFS. This is the device that will be formatted and mounted for application data.
      Specified by:
      ebsDeviceName in interface ApplicationSpec
      Returns:
      EBS device path (e.g., "/dev/xvdh")
    • ec2DataPath

      public String ec2DataPath()
      Description copied from interface: ApplicationSpec
      Returns the EC2 data path where application stores persistent data. This may differ from containerDataPath depending on application packaging.
      Specified by:
      ec2DataPath in interface ApplicationSpec
      Returns:
      EC2 mount path (e.g., "/var/lib/jenkins")
    • ec2LogPaths

      public List<String> ec2LogPaths()
      Description copied from interface: ApplicationSpec
      Returns CloudWatch log file paths for EC2 monitoring. These files will be streamed to CloudWatch Logs for centralized logging.
      Specified by:
      ec2LogPaths in interface ApplicationSpec
      Returns:
      list of absolute log file paths (e.g., ["/var/log/jenkins/jenkins.log"])
    • configureUserData

      public void configureUserData(UserDataBuilder builder, Ec2Context context)
      Description copied from interface: ApplicationSpec
      Configure EC2 UserData script for application installation and setup.

      The implementation should use the UserDataBuilder to add application-specific installation commands while leveraging infrastructure helpers for storage mounting and CloudWatch configuration.

      The infrastructure handles:

      • System updates
      • EFS vs EBS storage mounting (based on availability)
      • CloudWatch Agent installation and configuration
      • File permissions and ownership

      The application provides:

      • Application installation commands (yum/dnf install, etc.)
      • Application configuration
      • Service startup commands
      Specified by:
      configureUserData in interface ApplicationSpec
      Parameters:
      builder - The UserDataBuilder providing infrastructure helpers
      context - The Ec2Context providing runtime information
    • containerEnvironmentVariables

      public Map<String,String> containerEnvironmentVariables(String fqdn, boolean sslEnabled, String authMode)
      Description copied from interface: ApplicationSpec
      Configures application-specific environment variables for the container.

      Applications can override this to provide custom environment variables based on deployment configuration (FQDN, SSL, authMode, etc.). The infrastructure passes the FQDN, SSL settings, and authentication mode for applications that need reverse proxy configuration or authentication-specific setup.

      Example use cases:

      • Jenkins: JAVA_OPTS, JENKINS_OPTS for reverse proxy configuration, skip setup wizard for application-oidc
      • GitLab: GITLAB_OMNIBUS_CONFIG for external URL configuration and OIDC setup
      • Vault: VAULT_ADDR for API endpoint configuration
      Specified by:
      containerEnvironmentVariables in interface ApplicationSpec
      Parameters:
      fqdn - The fully qualified domain name (may be null)
      sslEnabled - Whether SSL is enabled
      authMode - The authentication mode (may be null, e.g., "none", "alb-oidc", "application-oidc")
      Returns:
      Map of environment variable key-value pairs (never null, may be empty)
    • autoAdminPasswordEnvVar

      public String autoAdminPasswordEnvVar()
      Description copied from interface: ApplicationSpec
      The container environment variable name a generated initial-admin-password secret should be bound to, or null (the default) if this application has no such variable.

      Mirrors CmsSpec.databaseEnvVars's existing pattern for the database password specifically: a database connection's host/port/name/user travel as plain strings through ApplicationSpec.containerEnvironmentVariables(String, boolean, String), but the password itself is never a plaintext value an app spec method returns — it's delivered as a Secrets Manager-backed ECS Secret, which needs a CDK construct only cloudforge-api can build. This method is the same shape, generalized to any application whose official image supports a fully non-interactive first-run install once its site name/admin account/admin password are all present (Joomla's own docker-entrypoint.sh is the first example — see JoomlaApplicationSpec), instead of a per-application name check hardcoded into ApplicationFactory/ ContainerFactory.

      Specified by:
      autoAdminPasswordEnvVar in interface ApplicationSpec
      Returns:
      the env var name (e.g. "JOOMLA_ADMIN_PASSWORD"), or null
    • supportsOidcIntegration

      public boolean supportsOidcIntegration()
      Description copied from interface: CmsSpec
      Check if this CMS supports OIDC integration.

      Overrides ApplicationSpec.supportsOidcIntegration() to read from CmsPlugin annotation instead of ApplicationPlugin.

      Specified by:
      supportsOidcIntegration in interface ApplicationSpec
      Specified by:
      supportsOidcIntegration in interface CmsSpec
      Returns:
      true if OIDC is supported based on @CmsPlugin.supportsOidc()
    • getOidcIntegration

      public OidcIntegration getOidcIntegration()
      Description copied from interface: ApplicationSpec
      Returns the OIDC integration handler for this application.

      This provides application-specific configuration for integrating with Cognito or IAM Identity Center OIDC.

      Specified by:
      getOidcIntegration in interface ApplicationSpec
      Returns:
      OIDC integration handler, or null if not supported
    • getSupportedAuthModes

      public List<String> getSupportedAuthModes()
      Description copied from interface: ApplicationSpec
      Returns the list of supported authentication modes for this application.

      CloudForge supports three authentication modes:

      • application-oidc: OIDC authentication integrated within the application (requires getOidcIntegration() != null)
      • alb-oidc: OIDC authentication at ALB level (works for all applications)
      • none: No authentication (public access or manually configured)

      The list is ordered by preference. The first mode is the recommended default.

      Default behavior:

      • If application has OIDC integration → ["application-oidc", "alb-oidc", "none"]
      • If application claims OIDC support but lacks integration → ["alb-oidc", "none"]
      • If application doesn't support OIDC → ["none"]
      Specified by:
      getSupportedAuthModes in interface ApplicationSpec
      Returns:
      List of supported auth modes in order of preference (never null, never empty)