0
# Value Formatting and Conversion
1
2
Play Framework's formatting system provides type-safe value parsing and formatting capabilities with extensible formatter registration. The system handles conversion between strings and typed objects with locale support, enabling automatic form data binding and custom type handling.
3
4
## Capabilities
5
6
### Core Formatting Operations
7
8
Central formatting utilities for parsing and printing values with type safety and locale support.
9
10
```java { .api }
11
/**
12
* Helper class for parsing and formatting values with type safety
13
*/
14
public class Formatters {
15
/** Parse string to specified type using default locale */
16
public static <T> T parse(String text, Class<T> clazz);
17
18
/** Parse string to specified type with field context */
19
public static <T> T parse(Field field, String text, Class<T> clazz);
20
21
/** Convert typed value to string representation */
22
public static <T> String print(T t);
23
24
/** Convert typed value to string with field context */
25
public static <T> String print(Field field, T t);
26
27
/** Convert typed value to string with type descriptor */
28
public static <T> String print(TypeDescriptor desc, T t);
29
30
/** Register simple formatter for a type */
31
public static <T> void register(Class<T> clazz, SimpleFormatter<T> formatter);
32
33
/** Register annotation-based formatter for a type */
34
public static <A extends Annotation,T> void register(Class<T> clazz, AnnotationFormatter<A,T> formatter);
35
36
/** The underlying Spring conversion service */
37
public static final FormattingConversionService conversion;
38
}
39
```
40
41
**Usage Examples:**
42
43
```java
44
import play.data.format.Formatters;
45
import java.util.Date;
46
import java.math.BigDecimal;
47
48
// Basic parsing and printing
49
String numberStr = "123.45";
50
BigDecimal number = Formatters.parse(numberStr, BigDecimal.class);
51
String formatted = Formatters.print(number); // "123.45"
52
53
// Date parsing with locale
54
String dateStr = "2023-12-25";
55
Date date = Formatters.parse(dateStr, Date.class);
56
String dateFormatted = Formatters.print(date); // Locale-specific format
57
58
// Custom type conversion
59
public class Temperature {
60
private double celsius;
61
public Temperature(double celsius) { this.celsius = celsius; }
62
public double getCelsius() { return celsius; }
63
}
64
65
// Register custom formatter
66
Formatters.register(Temperature.class, new SimpleFormatter<Temperature>() {
67
@Override
68
public Temperature parse(String text, Locale locale) {
69
return new Temperature(Double.parseDouble(text));
70
}
71
72
@Override
73
public String print(Temperature temp, Locale locale) {
74
return String.format("%.1f°C", temp.getCelsius());
75
}
76
});
77
```
78
79
### Simple Formatter Framework
80
81
Base class for creating simple, locale-aware formatters for custom types.
82
83
```java { .api }
84
/**
85
* Base class for simple formatters that handle parsing and printing
86
*/
87
public abstract class Formatters.SimpleFormatter<T> {
88
/** Parse text representation to typed object */
89
public abstract T parse(String text, Locale locale) throws ParseException;
90
91
/** Format typed object to text representation */
92
public abstract String print(T t, Locale locale);
93
}
94
```
95
96
**Usage Examples:**
97
98
```java
99
import play.data.format.Formatters.SimpleFormatter;
100
101
// Custom currency formatter
102
public class CurrencyFormatter extends SimpleFormatter<Money> {
103
@Override
104
public Money parse(String text, Locale locale) throws ParseException {
105
// Remove currency symbols and parse
106
String cleanText = text.replaceAll("[^\\d.,]", "");
107
BigDecimal amount = new BigDecimal(cleanText);
108
return new Money(amount, Currency.getInstance(locale));
109
}
110
111
@Override
112
public String print(Money money, Locale locale) {
113
NumberFormat formatter = NumberFormat.getCurrencyInstance(locale);
114
return formatter.format(money.getAmount());
115
}
116
}
117
118
// Register the formatter
119
Formatters.register(Money.class, new CurrencyFormatter());
120
121
// Usage in forms
122
public class ProductForm {
123
public String name;
124
public Money price; // Will use CurrencyFormatter automatically
125
}
126
```
127
128
### Annotation-Based Formatting
129
130
Advanced formatter framework for annotation-driven formatting with custom configuration.
131
132
```java { .api }
133
/**
134
* Base class for annotation-based formatters with custom configuration
135
*/
136
public abstract class Formatters.AnnotationFormatter<A extends Annotation, T> {
137
/** Parse text with annotation configuration */
138
public abstract T parse(A annotation, String text, Locale locale) throws ParseException;
139
140
/** Format value with annotation configuration */
141
public abstract String print(A annotation, T value, Locale locale);
142
}
143
```
144
145
### Built-in Format Types
146
147
Pre-built formatters and annotations for common data types.
148
149
```java { .api }
150
/**
151
* Container for built-in formatters and format annotations
152
*/
153
public class Formats {
154
// Built-in formatters are automatically registered
155
}
156
157
/**
158
* Date formatting with custom pattern support
159
*/
160
public class Formats.DateFormatter extends Formatters.SimpleFormatter<Date> {
161
/** Create date formatter with specific pattern */
162
public DateFormatter(String pattern);
163
164
/** Parse date string using configured pattern */
165
public Date parse(String text, Locale locale) throws ParseException;
166
167
/** Format date using configured pattern */
168
public String print(Date value, Locale locale);
169
}
170
171
/**
172
* Annotation for specifying date format patterns
173
*/
174
@interface Formats.DateTime {
175
/** Date format pattern (e.g., "yyyy-MM-dd", "dd/MM/yyyy HH:mm") */
176
String pattern();
177
}
178
179
/**
180
* Annotation-driven date formatter using @DateTime configuration
181
*/
182
public class Formats.AnnotationDateFormatter extends Formatters.AnnotationFormatter<Formats.DateTime, Date> {
183
public Date parse(Formats.DateTime annotation, String text, Locale locale) throws ParseException;
184
public String print(Formats.DateTime annotation, Date value, Locale locale);
185
}
186
187
/**
188
* Annotation for non-empty string validation and formatting
189
*/
190
@interface Formats.NonEmpty {}
191
192
/**
193
* Formatter for @NonEmpty annotation handling
194
*/
195
public class Formats.AnnotationNonEmptyFormatter extends Formatters.AnnotationFormatter<Formats.NonEmpty, String> {
196
public String parse(Formats.NonEmpty annotation, String text, Locale locale) throws ParseException;
197
public String print(Formats.NonEmpty annotation, String value, Locale locale);
198
}
199
```
200
201
**Usage Examples:**
202
203
```java
204
import play.data.format.Formats.*;
205
206
public class EventForm {
207
@DateTime(pattern = "yyyy-MM-dd")
208
public Date startDate;
209
210
@DateTime(pattern = "yyyy-MM-dd HH:mm")
211
public Date endDateTime;
212
213
@NonEmpty
214
public String title;
215
216
public String description;
217
}
218
219
// The formatters will automatically handle conversion
220
public Result createEvent() {
221
Form<EventForm> form = Form.form(EventForm.class).bindFromRequest();
222
223
if (form.hasErrors()) {
224
return badRequest(form.errorsAsJson());
225
}
226
227
EventForm event = form.get();
228
// Dates are automatically parsed according to their patterns
229
return ok("Event created: " + event.title);
230
}
231
```
232
233
## Advanced Usage Patterns
234
235
### Custom Annotation Formatters
236
237
```java
238
// Custom annotation for phone number formatting
239
@Retention(RetentionPolicy.RUNTIME)
240
@Target(ElementType.FIELD)
241
public @interface PhoneNumber {
242
String region() default "US";
243
boolean international() default false;
244
}
245
246
// Custom annotation formatter
247
public class PhoneNumberFormatter extends Formatters.AnnotationFormatter<PhoneNumber, String> {
248
249
@Override
250
public String parse(PhoneNumber annotation, String text, Locale locale) throws ParseException {
251
// Parse and validate phone number based on region
252
PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
253
try {
254
Phonenumber.PhoneNumber number = phoneUtil.parse(text, annotation.region());
255
if (!phoneUtil.isValidNumber(number)) {
256
throw new ParseException("Invalid phone number", 0);
257
}
258
return phoneUtil.format(number, PhoneNumberUtil.PhoneNumberFormat.E164);
259
} catch (NumberParseException e) {
260
throw new ParseException("Invalid phone number format", 0);
261
}
262
}
263
264
@Override
265
public String print(PhoneNumber annotation, String phoneNumber, Locale locale) {
266
PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
267
try {
268
Phonenumber.PhoneNumber number = phoneUtil.parse(phoneNumber, null);
269
PhoneNumberUtil.PhoneNumberFormat format = annotation.international()
270
? PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL
271
: PhoneNumberUtil.PhoneNumberFormat.NATIONAL;
272
return phoneUtil.format(number, format);
273
} catch (NumberParseException e) {
274
return phoneNumber; // Return as-is if can't format
275
}
276
}
277
}
278
279
// Register the custom formatter
280
Formatters.register(String.class, new PhoneNumberFormatter());
281
282
// Usage in model
283
public class ContactForm {
284
@PhoneNumber(region = "US", international = false)
285
public String phoneNumber;
286
287
@PhoneNumber(region = "US", international = true)
288
public String internationalPhone;
289
}
290
```
291
292
### Locale-Specific Formatting
293
294
```java
295
// Locale-aware number formatter
296
public class LocalizedNumberFormatter extends SimpleFormatter<BigDecimal> {
297
298
@Override
299
public BigDecimal parse(String text, Locale locale) throws ParseException {
300
NumberFormat format = NumberFormat.getNumberInstance(locale);
301
Number number = format.parse(text);
302
return new BigDecimal(number.toString());
303
}
304
305
@Override
306
public String print(BigDecimal value, Locale locale) {
307
NumberFormat format = NumberFormat.getNumberInstance(locale);
308
return format.format(value);
309
}
310
}
311
312
// Usage with different locales
313
public Result processLocalizedForm() {
314
// Request locale affects formatting
315
Locale currentLocale = request().getLocale();
316
317
Form<ProductForm> form = Form.form(ProductForm.class).bindFromRequest();
318
// Numbers will be parsed according to current locale
319
320
return ok("Form processed with locale: " + currentLocale);
321
}
322
```
323
324
### Complex Type Formatting
325
326
```java
327
// Custom formatter for complex types
328
public class CoordinateFormatter extends SimpleFormatter<Coordinate> {
329
330
@Override
331
public Coordinate parse(String text, Locale locale) throws ParseException {
332
// Parse "lat,lng" format
333
String[] parts = text.split(",");
334
if (parts.length != 2) {
335
throw new ParseException("Invalid coordinate format", 0);
336
}
337
338
try {
339
double lat = Double.parseDouble(parts[0].trim());
340
double lng = Double.parseDouble(parts[1].trim());
341
return new Coordinate(lat, lng);
342
} catch (NumberFormatException e) {
343
throw new ParseException("Invalid coordinate values", 0);
344
}
345
}
346
347
@Override
348
public String print(Coordinate coord, Locale locale) {
349
return String.format("%.6f,%.6f", coord.getLatitude(), coord.getLongitude());
350
}
351
}
352
353
// Model using complex formatter
354
public class LocationForm {
355
@Required
356
public String name;
357
358
public Coordinate coordinates; // Uses CoordinateFormatter
359
360
public String description;
361
}
362
```
363
364
### Error Handling in Formatters
365
366
```java
367
public class SafeNumberFormatter extends SimpleFormatter<Integer> {
368
369
@Override
370
public Integer parse(String text, Locale locale) throws ParseException {
371
if (text == null || text.trim().isEmpty()) {
372
return null; // Allow empty values
373
}
374
375
try {
376
return Integer.parseInt(text.trim());
377
} catch (NumberFormatException e) {
378
throw new ParseException("Invalid number: " + text, 0);
379
}
380
}
381
382
@Override
383
public String print(Integer value, Locale locale) {
384
return value != null ? value.toString() : "";
385
}
386
}
387
388
// Formatter registration with error handling
389
public class FormatterConfig {
390
public static void registerCustomFormatters() {
391
try {
392
Formatters.register(Coordinate.class, new CoordinateFormatter());
393
Formatters.register(Integer.class, new SafeNumberFormatter());
394
} catch (Exception e) {
395
Logger.error("Failed to register custom formatters", e);
396
// Fallback to default formatters
397
}
398
}
399
}
400
```