|
0
|
1 |
package hirondelle.web4j.model;
|
|
|
2 |
|
|
|
3 |
import hirondelle.web4j.BuildImpl;
|
|
|
4 |
import hirondelle.web4j.request.Formats;
|
|
|
5 |
import hirondelle.web4j.request.RequestParameter;
|
|
|
6 |
import hirondelle.web4j.ui.translate.Translator;
|
|
|
7 |
import hirondelle.web4j.util.Args;
|
|
|
8 |
import hirondelle.web4j.util.EscapeChars;
|
|
|
9 |
import hirondelle.web4j.util.Util;
|
|
|
10 |
|
|
|
11 |
import java.io.IOException;
|
|
|
12 |
import java.io.ObjectInputStream;
|
|
|
13 |
import java.io.Serializable;
|
|
|
14 |
import java.text.MessageFormat;
|
|
|
15 |
import java.util.ArrayList;
|
|
|
16 |
import java.util.Arrays;
|
|
|
17 |
import java.util.Collections;
|
|
|
18 |
import java.util.List;
|
|
|
19 |
import java.util.Locale;
|
|
|
20 |
import java.util.TimeZone;
|
|
|
21 |
import java.util.logging.Logger;
|
|
|
22 |
import java.util.regex.Matcher;
|
|
|
23 |
import java.util.regex.Pattern;
|
|
|
24 |
|
|
|
25 |
/**
|
|
|
26 |
Informative message presented to the end user.
|
|
|
27 |
|
|
|
28 |
<P>This class exists in order to hide the difference between <em>simple</em> and
|
|
|
29 |
<em>compound</em> messages.
|
|
|
30 |
|
|
|
31 |
<P><a name="SimpleMessage"></a><b>Simple Messages</b><br>
|
|
|
32 |
Simple messages are a single {@link String}, such as <tt>'Item deleted successfully.'</tt>.
|
|
|
33 |
They are created using {@link #forSimple(String)}.
|
|
|
34 |
|
|
|
35 |
<P><a name="CompoundMessage"></a><b>Compound Messages</b><br>
|
|
|
36 |
Compound messages are made up of several parts, and have parameters. They are created
|
|
|
37 |
using {@link #forCompound(String, Object...)}. A compound message
|
|
|
38 |
is usually implemented in Java using {@link java.text.MessageFormat}. <span class="highlight">
|
|
|
39 |
However, <tt>MessageFormat</tt> is not used by this class, to avoid the following issues </span>:
|
|
|
40 |
<ul>
|
|
|
41 |
<li> the dreaded apostrophe problem. In <tt>MessageFormat</tt>, the apostrophe is a special
|
|
|
42 |
character, and must be escaped. This is highly unnatural for translators, and has been a
|
|
|
43 |
source of continual, bothersome errors. (This is the principal reason for not
|
|
|
44 |
using <tt>MessageFormat</tt>.)
|
|
|
45 |
<li>the <tt>{0}</tt> placeholders start at <tt>0</tt>, not <tt>1</tt>. Again, this is
|
|
|
46 |
unnatural for translators.
|
|
|
47 |
<li>the number of parameters cannot exceed <tt>10</tt>. (Granted, it is not often
|
|
|
48 |
that a large number of parameters are needed, but there is no reason why this
|
|
|
49 |
restriction should exist.)
|
|
|
50 |
<li>in general, {@link MessageFormat} is rather complicated in its details.
|
|
|
51 |
</ul>
|
|
|
52 |
|
|
|
53 |
<P><a name="CustomFormat"></a><b>Format of Compound Messages</b><br>
|
|
|
54 |
This class defines an alternative format to that defined by {@link java.text.MessageFormat}.
|
|
|
55 |
For example,
|
|
|
56 |
<PRE>
|
|
|
57 |
"At this restaurant, the _1_ meal costs _2_."
|
|
|
58 |
"On _2_, I am going to Manon's place to see _1_."
|
|
|
59 |
</PRE>
|
|
|
60 |
Here,
|
|
|
61 |
<ul>
|
|
|
62 |
<li>the placeholders appear as <tt>_1_</tt>, <tt>_2_</tt>, and so on.
|
|
|
63 |
They start at <tt>1</tt>, not <tt>0</tt>, and have no upper limit. There is no escaping
|
|
|
64 |
mechanism to allow the placeholder text to appear in the message 'as is'. The <tt>_i_</tt>
|
|
|
65 |
placeholders stand for an <tt>Object</tt>, and carry no format information.
|
|
|
66 |
<li>apostrophes can appear anywhere, and do not need to be escaped.
|
|
|
67 |
<li>the formats applied to the various parameters are taken from {@link Formats}.
|
|
|
68 |
If the default formatting applied by {@link Formats} is not desired, then the caller
|
|
|
69 |
can always manually format the parameter as a {@link String}. (The {@link Translator} may be used when
|
|
|
70 |
a different pattern is needed for different Locales.)
|
|
|
71 |
<li>the number of parameters passed at runtime must match exactly the number of <tt>_i_</tt>
|
|
|
72 |
placeholders
|
|
|
73 |
</ul>
|
|
|
74 |
|
|
|
75 |
<P><b>Multilingual Applications</b><br>
|
|
|
76 |
Multilingual applications will need to ensure that messages can be successfully translated when
|
|
|
77 |
presented in JSPs. In particular, some care must be exercised to <em>not</em> create
|
|
|
78 |
a <em>simple</em> message out of various pieces of data when a <em>compound</em> message
|
|
|
79 |
should be used instead. See {@link #getMessage(Locale, TimeZone)}.
|
|
|
80 |
As well, see the <a href="../ui/translate/package-summary.html">hirondelle.web4j.ui.translate</a>
|
|
|
81 |
package for more information, in particular the
|
|
|
82 |
{@link hirondelle.web4j.ui.translate.Messages} tag used for rendering <tt>AppResponseMessage</tt>s,
|
|
|
83 |
even in single language applications.
|
|
|
84 |
|
|
|
85 |
<P><b>Serialization</b><br>
|
|
|
86 |
This class implements {@link Serializable} to allow messages stored in session scope to
|
|
|
87 |
be transferred over a network, and thus survive a failover operation.
|
|
|
88 |
<i>However, this class's implementation of Serializable interface has a minor defect.</i>
|
|
|
89 |
This class accepts <tt>Object</tt>s as parameters to messages. These objects almost always represent
|
|
|
90 |
data - String, Integer, Id, DateTime, and so on, and all such building block classes are Serializable.
|
|
|
91 |
If, however, the caller passes an unusual message parameter object which is not Serializable, then the
|
|
|
92 |
serialization of this object (if it occurs), will fail.
|
|
|
93 |
|
|
|
94 |
<P>The above defect will likely not be fixed since it has large ripple effects, and would seem to cause
|
|
|
95 |
more problems than it would solve. In retrospect, this the message parameters passed to
|
|
|
96 |
{@link #forCompound(String, Object[])} should likely have been typed as Serializable, not Object.
|
|
|
97 |
*/
|
|
|
98 |
public final class AppResponseMessage implements Serializable {
|
|
|
99 |
|
|
|
100 |
/**
|
|
|
101 |
<a href="#SimpleMessage">Simple message</a> having no parameters.
|
|
|
102 |
<tt>aSimpleText</tt> must have content.
|
|
|
103 |
*/
|
|
|
104 |
public static AppResponseMessage forSimple(String aSimpleText){
|
|
|
105 |
return new AppResponseMessage(aSimpleText, NO_PARAMS);
|
|
|
106 |
}
|
|
|
107 |
|
|
|
108 |
/**
|
|
|
109 |
<a href="#CompoundMessage">Compound message</a> having parameters.
|
|
|
110 |
|
|
|
111 |
<P><tt>aPattern</tt> follows the <a href="#CustomFormat">custom format</a> defined by this class.
|
|
|
112 |
{@link Formats#objectToTextForReport} will be used to format all parameters.
|
|
|
113 |
|
|
|
114 |
@param aPattern must be in the style of the <a href="#CustomFormat">custom format</a>, and
|
|
|
115 |
the number of placeholders must match the number of items in <tt>aParams</tt>.
|
|
|
116 |
@param aParams must have at least one member; all members must be non-null, but may be empty
|
|
|
117 |
{@link String}s.
|
|
|
118 |
*/
|
|
|
119 |
public static AppResponseMessage forCompound(String aPattern, Object... aParams){
|
|
|
120 |
if ( aParams.length < 1 ){
|
|
|
121 |
throw new IllegalArgumentException("Compound messages must have at least one parameter.");
|
|
|
122 |
}
|
|
|
123 |
return new AppResponseMessage(aPattern, aParams);
|
|
|
124 |
}
|
|
|
125 |
|
|
|
126 |
/**
|
|
|
127 |
Return either the 'simple text' or the <em>formatted</em> pattern with all parameter data rendered,
|
|
|
128 |
according to which factory method was called.
|
|
|
129 |
|
|
|
130 |
<P>The configured {@link Translator} is used to localize
|
|
|
131 |
<ul>
|
|
|
132 |
<li>the text passed to {@link #forSimple(String)}
|
|
|
133 |
<li>the pattern passed to {@link #forCompound(String, Object...)}
|
|
|
134 |
<li>any {@link hirondelle.web4j.request.RequestParameter} parameters passed to {@link #forCompound(String, Object...)}
|
|
|
135 |
are localized by using {@link Translator} on the return value of {@link RequestParameter#getName()}
|
|
|
136 |
(This is intended for displaying localized versions of control names.)
|
|
|
137 |
</ul>
|
|
|
138 |
|
|
|
139 |
<P>It is highly recommended that this method be called <em>late</em> in processing, in a JSP.
|
|
|
140 |
|
|
|
141 |
<P>The <tt>Locale</tt> should almost always come from
|
|
|
142 |
{@link hirondelle.web4j.BuildImpl#forLocaleSource()}.
|
|
|
143 |
The <tt>aLocale</tt> parameter is always required, even though there are cases when it
|
|
|
144 |
is not actually used to render the result.
|
|
|
145 |
*/
|
|
|
146 |
public String getMessage(Locale aLocale, TimeZone aTimeZone){
|
|
|
147 |
String result = null;
|
|
|
148 |
Translator translator = BuildImpl.forTranslator();
|
|
|
149 |
Formats formats = new Formats(aLocale, aTimeZone);
|
|
|
150 |
if( fParams.isEmpty() ){
|
|
|
151 |
result = translator.get(fText, aLocale);
|
|
|
152 |
}
|
|
|
153 |
else {
|
|
|
154 |
String localizedPattern = translator.get(fText, aLocale);
|
|
|
155 |
List<String> formattedParams = new ArrayList<String>();
|
|
|
156 |
for (Object param : fParams){
|
|
|
157 |
if ( param instanceof RequestParameter ){
|
|
|
158 |
RequestParameter reqParam = (RequestParameter)param;
|
|
|
159 |
String translatedParamName = translator.get(reqParam.getName(), aLocale);
|
|
|
160 |
formattedParams.add( translatedParamName );
|
|
|
161 |
}
|
|
|
162 |
else {
|
|
|
163 |
//this will escape any special HTML chars in params :
|
|
|
164 |
formattedParams.add( formats.objectToTextForReport(param).toString() );
|
|
|
165 |
}
|
|
|
166 |
}
|
|
|
167 |
result = populateParamsIntoCustomFormat(localizedPattern, formattedParams);
|
|
|
168 |
}
|
|
|
169 |
return result;
|
|
|
170 |
}
|
|
|
171 |
|
|
|
172 |
/**
|
|
|
173 |
Return an unmodifiable <tt>List</tt> corresponding to the <tt>aParams</tt> passed to
|
|
|
174 |
the constructor.
|
|
|
175 |
|
|
|
176 |
<P>If no parameters are being used, then return an empty list.
|
|
|
177 |
*/
|
|
|
178 |
public List<Object> getParams(){
|
|
|
179 |
return Collections.unmodifiableList(fParams);
|
|
|
180 |
}
|
|
|
181 |
|
|
|
182 |
/**
|
|
|
183 |
Return either the 'simple text' or the pattern, according to which factory method
|
|
|
184 |
was called. Typically, this method is <em>not</em> used to present text to the user (see {@link #getMessage}).
|
|
|
185 |
*/
|
|
|
186 |
@Override public String toString(){
|
|
|
187 |
return fText;
|
|
|
188 |
}
|
|
|
189 |
|
|
|
190 |
@Override public boolean equals(Object aThat){
|
|
|
191 |
Boolean result = ModelUtil.quickEquals(this, aThat);
|
|
|
192 |
if ( result == null ){
|
|
|
193 |
AppResponseMessage that = (AppResponseMessage) aThat;
|
|
|
194 |
result = ModelUtil.equalsFor(this.getSignificantFields(), that.getSignificantFields());
|
|
|
195 |
}
|
|
|
196 |
return result;
|
|
|
197 |
}
|
|
|
198 |
|
|
|
199 |
@Override public int hashCode(){
|
|
|
200 |
return ModelUtil.hashCodeFor(getSignificantFields());
|
|
|
201 |
}
|
|
|
202 |
|
|
|
203 |
// PRIVATE
|
|
|
204 |
|
|
|
205 |
/** Holds either the simple text, or the custom pattern. */
|
|
|
206 |
private final String fText;
|
|
|
207 |
|
|
|
208 |
/** List of Objects holds the parameters. Empty List if no parameters used. */
|
|
|
209 |
private final List<Object> fParams;
|
|
|
210 |
|
|
|
211 |
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("_(\\d)+_");
|
|
|
212 |
private static final Object[] NO_PARAMS = new Object[0];
|
|
|
213 |
private static final Logger fLogger = Util.getLogger(AppResponseMessage.class);
|
|
|
214 |
|
|
|
215 |
private static final long serialVersionUID = 1000L;
|
|
|
216 |
|
|
|
217 |
private AppResponseMessage(String aText, Object... aParams){
|
|
|
218 |
fText = aText;
|
|
|
219 |
fParams = Arrays.asList(aParams);
|
|
|
220 |
validateState();
|
|
|
221 |
}
|
|
|
222 |
|
|
|
223 |
private void validateState(){
|
|
|
224 |
Args.checkForContent(fText);
|
|
|
225 |
if (fParams != null && fParams.size() > 0){
|
|
|
226 |
for(Object item : fParams){
|
|
|
227 |
if ( item == null ){
|
|
|
228 |
throw new IllegalArgumentException("Parameters to compound messages must be non-null.");
|
|
|
229 |
}
|
|
|
230 |
}
|
|
|
231 |
}
|
|
|
232 |
}
|
|
|
233 |
|
|
|
234 |
/**
|
|
|
235 |
@param aFormattedParams contains Strings ready to be placed in to the pattern. The index <tt>i</tt> of the
|
|
|
236 |
List matches the <tt>_i_</tt> placeholder. The size of aFormattedParams must match the number of
|
|
|
237 |
placeholders.
|
|
|
238 |
*/
|
|
|
239 |
private String populateParamsIntoCustomFormat(String aPattern, List<String> aFormattedParams){
|
|
|
240 |
StringBuffer result = new StringBuffer();
|
|
|
241 |
fLogger.finest("Populating " + Util.quote(aPattern) + " with params " + Util.logOnePerLine(aFormattedParams));
|
|
|
242 |
Matcher matcher = PLACEHOLDER_PATTERN.matcher(aPattern);
|
|
|
243 |
int numMatches = 0;
|
|
|
244 |
while ( matcher.find() ) {
|
|
|
245 |
++numMatches;
|
|
|
246 |
if(numMatches > aFormattedParams.size()){
|
|
|
247 |
String message = "The number of placeholders exceeds the number of available parameters (" + aFormattedParams.size() + ")";
|
|
|
248 |
fLogger.severe(message);
|
|
|
249 |
throw new IllegalArgumentException(message);
|
|
|
250 |
}
|
|
|
251 |
matcher.appendReplacement(result, getReplacement(matcher, aFormattedParams));
|
|
|
252 |
}
|
|
|
253 |
if(numMatches < aFormattedParams.size()){
|
|
|
254 |
String message = "The number of placeholders (" + numMatches + ") is less than the number of available parameters (" + aFormattedParams.size() + ")";
|
|
|
255 |
fLogger.severe(message);
|
|
|
256 |
throw new IllegalArgumentException(message);
|
|
|
257 |
}
|
|
|
258 |
matcher.appendTail(result);
|
|
|
259 |
return result.toString();
|
|
|
260 |
}
|
|
|
261 |
|
|
|
262 |
private String getReplacement(Matcher aMatcher, List<String> aFormattedParams){
|
|
|
263 |
String result = null;
|
|
|
264 |
String digit = aMatcher.group(1);
|
|
|
265 |
int idx = Integer.parseInt(digit);
|
|
|
266 |
if(idx <= 0){
|
|
|
267 |
throw new IllegalArgumentException("Placeholder digit should be 1,2,3... but takes value " + idx);
|
|
|
268 |
}
|
|
|
269 |
if(idx > aFormattedParams.size()){
|
|
|
270 |
throw new IllegalArgumentException("Placeholder index for _" + idx + "_ exceeds the number of available parameters (" + aFormattedParams.size() + ")");
|
|
|
271 |
}
|
|
|
272 |
result = aFormattedParams.get(idx - 1);
|
|
|
273 |
return EscapeChars.forReplacementString(result);
|
|
|
274 |
}
|
|
|
275 |
|
|
|
276 |
private Object[] getSignificantFields(){
|
|
|
277 |
return new Object[] {fText, fParams};
|
|
|
278 |
}
|
|
|
279 |
|
|
|
280 |
/**
|
|
|
281 |
Always treat de-serialization as a full-blown constructor, by validating the final state of the deserialized object.
|
|
|
282 |
*/
|
|
|
283 |
private void readObject(ObjectInputStream aInputStream) throws ClassNotFoundException, IOException {
|
|
|
284 |
aInputStream.defaultReadObject();
|
|
|
285 |
validateState();
|
|
|
286 |
}
|
|
|
287 |
}
|