Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
Tapestry |
|
| 2.6666666666666665;2.667 |
1 | // Copyright 2004, 2005 The Apache Software Foundation | |
2 | // | |
3 | // Licensed under the Apache License, Version 2.0 (the "License"); | |
4 | // you may not use this file except in compliance with the License. | |
5 | // You may obtain a copy of the License at | |
6 | // | |
7 | // http://www.apache.org/licenses/LICENSE-2.0 | |
8 | // | |
9 | // Unless required by applicable law or agreed to in writing, software | |
10 | // distributed under the License is distributed on an "AS IS" BASIS, | |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 | // See the License for the specific language governing permissions and | |
13 | // limitations under the License. | |
14 | ||
15 | package org.apache.tapestry; | |
16 | ||
17 | import org.apache.hivemind.ApplicationRuntimeException; | |
18 | import org.apache.hivemind.Location; | |
19 | import org.apache.tapestry.event.ChangeObserver; | |
20 | import org.apache.tapestry.event.ObservedChangeEvent; | |
21 | import org.apache.tapestry.multipart.IMultipartDecoder; | |
22 | import org.apache.tapestry.spec.IComponentSpecification; | |
23 | import org.apache.tapestry.util.StringSplitter; | |
24 | ||
25 | import java.io.IOException; | |
26 | import java.io.InputStream; | |
27 | import java.text.MessageFormat; | |
28 | import java.util.*; | |
29 | ||
30 | /** | |
31 | * A placeholder for a number of (static) methods that don't belong elsewhere, as well as a global | |
32 | * location for static constants. | |
33 | * | |
34 | * @since 1.0.1 | |
35 | * @author Howard Lewis Ship | |
36 | */ | |
37 | ||
38 | public final class Tapestry | |
39 | { | |
40 | /** | |
41 | * The name ("direct") of a service that allows stateless behavior for an {@link | |
42 | * org.apache.tapestry.link.DirectLink} component. | |
43 | * <p> | |
44 | * This service rolls back the state of the page but doesn't rewind the the dynamic state of the | |
45 | * page the was the action service does, which is more efficient but less powerful. | |
46 | * <p> | |
47 | * An array of String parameters may be included with the service URL; these will be made | |
48 | * available to the {@link org.apache.tapestry.link.DirectLink} component's listener. | |
49 | */ | |
50 | ||
51 | public static final String DIRECT_SERVICE = "direct"; | |
52 | ||
53 | /** | |
54 | * Almost identical to the direct service, except specifically for handling | |
55 | * browser level events. | |
56 | * | |
57 | * @since 4.1 | |
58 | */ | |
59 | ||
60 | public static final String DIRECT_EVENT_SERVICE = "directevent"; | |
61 | ||
62 | /** | |
63 | * The name ("external") of a service that a allows {@link IExternalPage} to be selected. | |
64 | * Associated with a {@link org.apache.tapestry.link.ExternalLink} component. | |
65 | * <p> | |
66 | * This service enables {@link IExternalPage}s to be accessed via a URL. External pages may be | |
67 | * booked marked using their URL for future reference. | |
68 | * <p> | |
69 | * An array of Object parameters may be included with the service URL; these will be passed to | |
70 | * the {@link IExternalPage#activateExternalPage(Object[], IRequestCycle)} method. | |
71 | */ | |
72 | ||
73 | public static final String EXTERNAL_SERVICE = "external"; | |
74 | ||
75 | /** | |
76 | * The name ("page") of a service that allows a new page to be selected. Associated with a | |
77 | * {@link org.apache.tapestry.link.PageLink} component. | |
78 | * <p> | |
79 | * The service requires a single parameter: the name of the target page. | |
80 | */ | |
81 | ||
82 | public static final String PAGE_SERVICE = "page"; | |
83 | ||
84 | /** | |
85 | * The name ("home") of a service that jumps to the home page. A stand-in for when no service is | |
86 | * provided, which is typically the entrypoint to the application. | |
87 | */ | |
88 | ||
89 | public static final String HOME_SERVICE = "home"; | |
90 | ||
91 | /** | |
92 | * The name ("restart") of a service that invalidates the session and restarts the application. | |
93 | * Typically used just to recover from an exception. | |
94 | */ | |
95 | ||
96 | public static final String RESTART_SERVICE = "restart"; | |
97 | ||
98 | /** | |
99 | * The name ("asset") of a service used to access internal assets. | |
100 | */ | |
101 | ||
102 | public static final String ASSET_SERVICE = "asset"; | |
103 | ||
104 | /** | |
105 | * The name ("reset") of a service used to clear cached template and specification data and | |
106 | * remove all pooled pages. This is only used when debugging as a quick way to clear the out | |
107 | * cached data, to allow updated versions of specifications and templates to be loaded (without | |
108 | * stopping and restarting the servlet container). | |
109 | * <p> | |
110 | * This service is only available if the Java system property | |
111 | * <code>org.apache.tapestry.enable-reset-service</code> is set to <code>true</code>. | |
112 | */ | |
113 | ||
114 | public static final String RESET_SERVICE = "reset"; | |
115 | ||
116 | /** | |
117 | * Property name used to get the extension used for templates. This may be set in the page or | |
118 | * component specification, or in the page (or component's) immediate container (library or | |
119 | * application specification). Unlike most properties, value isn't inherited all the way up the | |
120 | * chain. The default template extension is "html". | |
121 | * | |
122 | * @since 3.0 | |
123 | */ | |
124 | ||
125 | public static final String TEMPLATE_EXTENSION_PROPERTY = "org.apache.tapestry.template-extension"; | |
126 | ||
127 | /** | |
128 | * The name of an {@link org.apache.tapestry.IRequestCycle} attribute in which the currently | |
129 | * rendering {@link org.apache.tapestry.components.ILinkComponent} is stored. Link components do | |
130 | * not nest. | |
131 | */ | |
132 | ||
133 | public static final String LINK_COMPONENT_ATTRIBUTE_NAME = "org.apache.tapestry.active-link-component"; | |
134 | ||
135 | /** | |
136 | * Suffix appended to a parameter name to form the name of a property that stores the binding | |
137 | * for the parameter. | |
138 | * | |
139 | * @since 3.0 | |
140 | */ | |
141 | ||
142 | public static final String PARAMETER_PROPERTY_NAME_SUFFIX = "Binding"; | |
143 | ||
144 | /** | |
145 | * Key used to obtain an extension from the application specification. The extension, if it | |
146 | * exists, implements {@link org.apache.tapestry.request.IRequestDecoder}. | |
147 | * | |
148 | * @since 2.2 | |
149 | */ | |
150 | ||
151 | public static final String REQUEST_DECODER_EXTENSION_NAME = "org.apache.tapestry.request-decoder"; | |
152 | ||
153 | /** | |
154 | * Name of optional application extension for the multipart decoder used by the application. The | |
155 | * extension must implement {@link org.apache.tapestry.multipart.IMultipartDecoder} (and is | |
156 | * generally a configured instance of | |
157 | * {@link IMultipartDecoder}). | |
158 | * | |
159 | * @since 3.0 | |
160 | */ | |
161 | ||
162 | public static final String MULTIPART_DECODER_EXTENSION_NAME = "org.apache.tapestry.multipart-decoder"; | |
163 | ||
164 | /** | |
165 | * Method id used to check that {@link IPage#validate(IRequestCycle)} is invoked. | |
166 | * | |
167 | * @see #checkMethodInvocation(Object, String, Object) | |
168 | * @since 3.0 | |
169 | */ | |
170 | ||
171 | public static final String ABSTRACTPAGE_VALIDATE_METHOD_ID = "AbstractPage.validate()"; | |
172 | ||
173 | /** | |
174 | * Method id used to check that {@link IPage#detach()} is invoked. | |
175 | * | |
176 | * @see #checkMethodInvocation(Object, String, Object) | |
177 | * @since 3.0 | |
178 | */ | |
179 | ||
180 | public static final String ABSTRACTPAGE_DETACH_METHOD_ID = "AbstractPage.detach()"; | |
181 | ||
182 | /** | |
183 | * Regular expression defining a simple property name. Used by several different parsers. Simple | |
184 | * property names match Java variable names; a leading letter (or underscore), followed by | |
185 | * letters, numbers and underscores. | |
186 | * | |
187 | * @since 3.0 | |
188 | */ | |
189 | ||
190 | public static final String SIMPLE_PROPERTY_NAME_PATTERN = "^_?[a-zA-Z]\\w*$"; | |
191 | ||
192 | /** | |
193 | * Class name of an {@link ognl.TypeConverter}implementing class to use as a type converter for | |
194 | * {@link org.apache.tapestry.binding.ExpressionBinding}. | |
195 | */ | |
196 | public static final String OGNL_TYPE_CONVERTER = "org.apache.tapestry.ognl-type-converter"; | |
197 | ||
198 | /** | |
199 | * The version of the framework; this is updated for major releases. | |
200 | */ | |
201 | ||
202 | 0 | public static final String VERSION = readVersion(); |
203 | ||
204 | private static final String UNKNOWN_VERSION = "Unknown"; | |
205 | ||
206 | /** | |
207 | * Contains strings loaded from TapestryStrings.properties. | |
208 | * | |
209 | * @since 1.0.8 | |
210 | */ | |
211 | ||
212 | private static ResourceBundle _strings; | |
213 | ||
214 | /** | |
215 | * A {@link Map}that links Locale names (as in {@link Locale#toString()}to {@link Locale} | |
216 | * instances. This prevents needless duplication of Locales. | |
217 | */ | |
218 | ||
219 | 0 | private static final Map _localeMap = new HashMap(); |
220 | ||
221 | static | |
222 | { | |
223 | 0 | Locale[] locales = Locale.getAvailableLocales(); |
224 | 0 | for (int i = 0; i < locales.length; i++) |
225 | { | |
226 | 0 | _localeMap.put(locales[i].toString(), locales[i]); |
227 | } | |
228 | } | |
229 | ||
230 | /** | |
231 | * Used for tracking if a particular super-class method has been invoked. | |
232 | */ | |
233 | ||
234 | 0 | private static final ThreadLocal _invokedMethodIds = new ThreadLocal(); |
235 | ||
236 | ||
237 | /** | |
238 | * Prevent instantiation. | |
239 | */ | |
240 | ||
241 | private Tapestry() | |
242 | 0 | { |
243 | 0 | } |
244 | ||
245 | /** | |
246 | * Copys all informal {@link IBinding bindings}from a source component to the destination | |
247 | * component. Informal bindings are bindings for informal parameters. This will overwrite | |
248 | * parameters (formal or informal) in the destination component if there is a naming conflict. | |
249 | */ | |
250 | ||
251 | public static void copyInformalBindings(IComponent source, IComponent destination) | |
252 | { | |
253 | 0 | Collection names = source.getBindingNames(); |
254 | ||
255 | 0 | if (names == null) |
256 | 0 | return; |
257 | ||
258 | 0 | IComponentSpecification specification = source.getSpecification(); |
259 | 0 | Iterator i = names.iterator(); |
260 | ||
261 | 0 | while (i.hasNext()) |
262 | { | |
263 | 0 | String name = (String) i.next(); |
264 | ||
265 | // If not a formal parameter, then copy it over. | |
266 | ||
267 | 0 | if (specification.getParameter(name) == null) |
268 | { | |
269 | 0 | IBinding binding = source.getBinding(name); |
270 | ||
271 | 0 | destination.setBinding(name, binding); |
272 | } | |
273 | 0 | } |
274 | 0 | } |
275 | ||
276 | /** | |
277 | * Gets the {@link Locale}for the given string, which is the result of | |
278 | * {@link Locale#toString()}. If no such locale is already registered, a new instance is | |
279 | * created, registered and returned. | |
280 | */ | |
281 | ||
282 | public static Locale getLocale(String s) | |
283 | { | |
284 | 0 | Locale result = null; |
285 | ||
286 | 0 | synchronized (_localeMap) |
287 | { | |
288 | 0 | result = (Locale) _localeMap.get(s); |
289 | 0 | } |
290 | ||
291 | 0 | if (result == null) |
292 | { | |
293 | 0 | StringSplitter splitter = new StringSplitter('_'); |
294 | 0 | String[] terms = splitter.splitToArray(s); |
295 | ||
296 | 0 | switch (terms.length) |
297 | { | |
298 | case 1: | |
299 | ||
300 | 0 | result = new Locale(terms[0], ""); |
301 | 0 | break; |
302 | ||
303 | case 2: | |
304 | ||
305 | 0 | result = new Locale(terms[0], terms[1]); |
306 | 0 | break; |
307 | ||
308 | case 3: | |
309 | ||
310 | 0 | result = new Locale(terms[0], terms[1], terms[2]); |
311 | 0 | break; |
312 | ||
313 | default: | |
314 | ||
315 | 0 | throw new IllegalArgumentException("Unable to convert '" + s + "' to a Locale."); |
316 | } | |
317 | ||
318 | 0 | synchronized (_localeMap) |
319 | { | |
320 | 0 | _localeMap.put(s, result); |
321 | 0 | } |
322 | ||
323 | } | |
324 | ||
325 | 0 | return result; |
326 | ||
327 | } | |
328 | ||
329 | /** | |
330 | * Closes the stream (if not null), ignoring any {@link IOException}thrown. | |
331 | * | |
332 | * @since 1.0.2 | |
333 | */ | |
334 | ||
335 | public static void close(InputStream stream) | |
336 | { | |
337 | 0 | if (stream != null) |
338 | { | |
339 | try | |
340 | { | |
341 | 0 | stream.close(); |
342 | } | |
343 | 0 | catch (IOException ex) |
344 | { | |
345 | // Ignore. | |
346 | 0 | } |
347 | } | |
348 | 0 | } |
349 | ||
350 | /** | |
351 | * Gets a string from the TapestryStrings resource bundle. The string in the bundle is treated | |
352 | * as a pattern for {@link MessageFormat#format(java.lang.String, java.lang.Object[])}. | |
353 | * | |
354 | * @since 1.0.8 | |
355 | */ | |
356 | ||
357 | public static String format(String key, Object[] args) | |
358 | { | |
359 | 0 | if (_strings == null) |
360 | 0 | _strings = ResourceBundle.getBundle("org.apache.tapestry.TapestryStrings"); |
361 | ||
362 | 0 | String pattern = _strings.getString(key); |
363 | ||
364 | 0 | if (args == null) |
365 | 0 | return pattern; |
366 | ||
367 | 0 | return MessageFormat.format(pattern, args); |
368 | } | |
369 | ||
370 | /** | |
371 | * Convienience method for invoking {@link #format(String, Object[])}. | |
372 | * | |
373 | * @since 3.0 | |
374 | */ | |
375 | ||
376 | public static String getMessage(String key) | |
377 | { | |
378 | 0 | return format(key, null); |
379 | } | |
380 | ||
381 | /** | |
382 | * Convienience method for invoking {@link #format(String, Object[])}. | |
383 | * | |
384 | * @since 3.0 | |
385 | */ | |
386 | ||
387 | public static String format(String key, Object arg) | |
388 | { | |
389 | 0 | return format(key, new Object[] |
390 | { arg }); | |
391 | } | |
392 | ||
393 | /** | |
394 | * Convienience method for invoking {@link #format(String, Object[])}. | |
395 | * | |
396 | * @since 3.0 | |
397 | */ | |
398 | ||
399 | public static String format(String key, Object arg1, Object arg2) | |
400 | { | |
401 | 0 | return format(key, new Object[] |
402 | { arg1, arg2 }); | |
403 | } | |
404 | ||
405 | /** | |
406 | * Convienience method for invoking {@link #format(String, Object[])}. | |
407 | * | |
408 | * @since 3.0 | |
409 | */ | |
410 | ||
411 | public static String format(String key, Object arg1, Object arg2, Object arg3) | |
412 | { | |
413 | 0 | return format(key, new Object[] |
414 | { arg1, arg2, arg3 }); | |
415 | } | |
416 | ||
417 | /** | |
418 | * Invoked when the class is initialized to read the current version file. | |
419 | */ | |
420 | ||
421 | private static String readVersion() | |
422 | { | |
423 | 0 | Properties props = new Properties(); |
424 | ||
425 | try | |
426 | { | |
427 | 0 | InputStream in = Tapestry.class.getResourceAsStream("version.properties"); |
428 | ||
429 | 0 | if (in == null) |
430 | 0 | return UNKNOWN_VERSION; |
431 | ||
432 | 0 | props.load(in); |
433 | ||
434 | 0 | in.close(); |
435 | ||
436 | 0 | return props.getProperty("project.version", UNKNOWN_VERSION); |
437 | } | |
438 | 0 | catch (IOException ex) |
439 | { | |
440 | 0 | return UNKNOWN_VERSION; |
441 | } | |
442 | ||
443 | } | |
444 | ||
445 | /** | |
446 | * Returns the size of a collection, or zero if the collection is null. | |
447 | * | |
448 | * @since 2.2 | |
449 | */ | |
450 | ||
451 | public static int size(Collection c) | |
452 | { | |
453 | 0 | if (c == null) |
454 | 0 | return 0; |
455 | ||
456 | 0 | return c.size(); |
457 | } | |
458 | ||
459 | /** | |
460 | * Returns the length of the array, or 0 if the array is null. | |
461 | * | |
462 | * @since 2.2 | |
463 | */ | |
464 | ||
465 | public static int size(Object[] array) | |
466 | { | |
467 | 0 | if (array == null) |
468 | 0 | return 0; |
469 | ||
470 | 0 | return array.length; |
471 | } | |
472 | ||
473 | /** | |
474 | * Returns true if the Map is null or empty. | |
475 | * | |
476 | * @since 3.0 | |
477 | */ | |
478 | ||
479 | public static boolean isEmpty(Map map) | |
480 | { | |
481 | 0 | return map == null || map.isEmpty(); |
482 | } | |
483 | ||
484 | /** | |
485 | * Returns true if the Collection is null or empty. | |
486 | * | |
487 | * @since 3.0 | |
488 | */ | |
489 | ||
490 | public static boolean isEmpty(Collection c) | |
491 | { | |
492 | 0 | return c == null || c.isEmpty(); |
493 | } | |
494 | ||
495 | /** | |
496 | * Converts a {@link Map} to an even-sized array of key/value pairs. This may be useful when | |
497 | * using a Map as service parameters (with {@link org.apache.tapestry.link.DirectLink}. | |
498 | * Assuming the keys and values are simple objects (String, Boolean, Integer, etc.), then the | |
499 | * representation as an array will encode more efficiently (via | |
500 | * {@link org.apache.tapestry.util.io.DataSqueezerImpl} than serializing the Map and its | |
501 | * contents. | |
502 | * | |
503 | * @return the array of keys and values, or null if the input Map is null or empty | |
504 | * @since 2.2 | |
505 | */ | |
506 | ||
507 | public static Object[] convertMapToArray(Map map) | |
508 | { | |
509 | 0 | if (isEmpty(map)) |
510 | 0 | return null; |
511 | ||
512 | 0 | Set entries = map.entrySet(); |
513 | ||
514 | 0 | Object[] result = new Object[2 * entries.size()]; |
515 | 0 | int x = 0; |
516 | ||
517 | 0 | Iterator i = entries.iterator(); |
518 | 0 | while (i.hasNext()) |
519 | { | |
520 | 0 | Map.Entry entry = (Map.Entry) i.next(); |
521 | ||
522 | 0 | result[x++] = entry.getKey(); |
523 | 0 | result[x++] = entry.getValue(); |
524 | 0 | } |
525 | ||
526 | 0 | return result; |
527 | } | |
528 | ||
529 | /** | |
530 | * Converts an even-sized array of objects back into a {@link Map}. | |
531 | * | |
532 | * @see #convertMapToArray(Map) | |
533 | * @return a Map, or null if the array is null or empty | |
534 | * @since 2.2 | |
535 | */ | |
536 | ||
537 | public static Map convertArrayToMap(Object[] array) | |
538 | { | |
539 | 0 | if (array == null || array.length == 0) |
540 | 0 | return null; |
541 | ||
542 | 0 | if (array.length % 2 != 0) |
543 | 0 | throw new IllegalArgumentException(getMessage("Tapestry.even-sized-array")); |
544 | ||
545 | 0 | Map result = new HashMap(); |
546 | ||
547 | 0 | int x = 0; |
548 | 0 | while (x < array.length) |
549 | { | |
550 | 0 | Object key = array[x++]; |
551 | 0 | Object value = array[x++]; |
552 | ||
553 | 0 | result.put(key, value); |
554 | 0 | } |
555 | ||
556 | 0 | return result; |
557 | } | |
558 | ||
559 | /** | |
560 | * Creates an exception indicating the binding value is null. | |
561 | * | |
562 | * @since 3.0 | |
563 | */ | |
564 | ||
565 | public static BindingException createNullBindingException(IBinding binding) | |
566 | { | |
567 | 0 | return new BindingException(getMessage("null-value-for-binding"), binding); |
568 | } | |
569 | ||
570 | /** @since 3.0 * */ | |
571 | ||
572 | public static ApplicationRuntimeException createNoSuchComponentException(IComponent component, | |
573 | String id, Location location) | |
574 | { | |
575 | 0 | return new ApplicationRuntimeException(format("no-such-component", component.getExtendedId(), id), |
576 | component, location, null); | |
577 | } | |
578 | ||
579 | /** @since 3.0 * */ | |
580 | ||
581 | public static BindingException createRequiredParameterException(IComponent component, | |
582 | String parameterName) | |
583 | { | |
584 | 0 | return new BindingException(format("required-parameter", parameterName, component.getExtendedId()), |
585 | component, null, component.getBinding(parameterName), null); | |
586 | } | |
587 | ||
588 | /** @since 3.0 * */ | |
589 | ||
590 | public static ApplicationRuntimeException createRenderOnlyPropertyException( | |
591 | IComponent component, String propertyName) | |
592 | { | |
593 | 0 | return new ApplicationRuntimeException(format("render-only-property", |
594 | propertyName, | |
595 | component.getExtendedId()), component, null, null); | |
596 | } | |
597 | ||
598 | /** | |
599 | * Clears the list of method invocations. | |
600 | * | |
601 | * @see #checkMethodInvocation(Object, String, Object) | |
602 | * @since 3.0 | |
603 | */ | |
604 | ||
605 | public static void clearMethodInvocations() | |
606 | { | |
607 | 0 | _invokedMethodIds.set(null); |
608 | 0 | } |
609 | ||
610 | /** | |
611 | * Adds a method invocation to the list of invocations. This is done in a super-class | |
612 | * implementations. | |
613 | * | |
614 | * @see #checkMethodInvocation(Object, String, Object) | |
615 | * @since 3.0 | |
616 | */ | |
617 | ||
618 | public static void addMethodInvocation(Object methodId) | |
619 | { | |
620 | 0 | List methodIds = (List) _invokedMethodIds.get(); |
621 | ||
622 | 0 | if (methodIds == null) |
623 | { | |
624 | 0 | methodIds = new ArrayList(); |
625 | 0 | _invokedMethodIds.set(methodIds); |
626 | } | |
627 | ||
628 | 0 | methodIds.add(methodId); |
629 | 0 | } |
630 | ||
631 | /** | |
632 | * Checks to see if a particular method has been invoked. The method is identified by a methodId | |
633 | * (usually a String). The methodName and object are used to create an error message. | |
634 | * <p> | |
635 | * The caller should invoke {@link #clearMethodInvocations()}, then invoke a method on the | |
636 | * object. The super-class implementation should invoke {@link #addMethodInvocation(Object)} to | |
637 | * indicate that it was, in fact, invoked. The caller then invokes this method to validate that | |
638 | * the super-class implementation was invoked. | |
639 | * <p> | |
640 | * The list of method invocations is stored in a {@link ThreadLocal} variable. | |
641 | * | |
642 | * @since 3.0 | |
643 | */ | |
644 | ||
645 | public static void checkMethodInvocation(Object methodId, String methodName, Object object) | |
646 | { | |
647 | 0 | List methodIds = (List) _invokedMethodIds.get(); |
648 | ||
649 | 0 | if (methodIds != null && methodIds.contains(methodId)) |
650 | 0 | return; |
651 | ||
652 | 0 | throw new ApplicationRuntimeException(Tapestry.format("Tapestry.missing-method-invocation", |
653 | object.getClass().getName(), | |
654 | methodName)); | |
655 | } | |
656 | ||
657 | /** | |
658 | * Method used by pages and components to send notifications about property changes. | |
659 | * | |
660 | * @param component | |
661 | * the component containing the property | |
662 | * @param propertyName | |
663 | * the name of the property which changed | |
664 | * @param newValue | |
665 | * the new value for the property | |
666 | * @since 3.0 | |
667 | */ | |
668 | public static void fireObservedChange(IComponent component, String propertyName, Object newValue) | |
669 | { | |
670 | 0 | ChangeObserver observer = component.getPage().getChangeObserver(); |
671 | ||
672 | 0 | if (observer == null) |
673 | 0 | return; |
674 | ||
675 | 0 | ObservedChangeEvent event = new ObservedChangeEvent(component, propertyName, newValue); |
676 | ||
677 | 0 | observer.observeChange(event); |
678 | 0 | } |
679 | } |