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