Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
PropertiesContext |
|
|
1.6521739130434783;1.652 | ||||
PropertiesContext$1 |
|
|
1.6521739130434783;1.652 | ||||
PropertiesContext$Builder |
|
|
1.6521739130434783;1.652 |
1 | /* |
|
2 | * Copyright 2012-2013 smartics, Kronseder & Reiner GmbH |
|
3 | * |
|
4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
|
5 | * you may not use this file except in compliance with the License. |
|
6 | * You may obtain a copy of the License at |
|
7 | * |
|
8 | * http://www.apache.org/licenses/LICENSE-2.0 |
|
9 | * |
|
10 | * Unless required by applicable law or agreed to in writing, software |
|
11 | * distributed under the License is distributed on an "AS IS" BASIS, |
|
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
13 | * See the License for the specific language governing permissions and |
|
14 | * limitations under the License. |
|
15 | */ |
|
16 | package de.smartics.properties.api.core.domain; |
|
17 | ||
18 | import java.io.Serializable; |
|
19 | import java.net.URL; |
|
20 | import java.util.ArrayList; |
|
21 | import java.util.List; |
|
22 | import java.util.Locale; |
|
23 | import java.util.regex.Pattern; |
|
24 | ||
25 | import javax.annotation.CheckForNull; |
|
26 | import javax.annotation.concurrent.ThreadSafe; |
|
27 | ||
28 | import org.apache.commons.lang.LocaleUtils; |
|
29 | import org.apache.commons.lang.StringUtils; |
|
30 | import org.apache.commons.lang.builder.ToStringBuilder; |
|
31 | ||
32 | import de.smartics.properties.api.core.context.alias.AliasTraverser; |
|
33 | import de.smartics.properties.api.core.context.alias.DuplicateAliasException; |
|
34 | import de.smartics.properties.api.core.context.alias.PropertyAliasMapping; |
|
35 | import de.smartics.properties.api.core.context.alias.UnknownAliasException; |
|
36 | import de.smartics.util.lang.Arg; |
|
37 | import de.smartics.util.lang.BlankArgumentException; |
|
38 | ||
39 | /** |
|
40 | * Defines the configuration for smartics properties configuration. |
|
41 | * <p> |
|
42 | * A context provides information for all its properties. Not all properties of |
|
43 | * an application may refer to the same context. |
|
44 | * </p> |
|
45 | * |
|
46 | * @impl For testing purposes please refer to |
|
47 | * <code>help.de.smartics.properties.core.PropertiesContextBuilder</code>. |
|
48 | */ |
|
49 | @ThreadSafe |
|
50 | 0 | public final class PropertiesContext implements Serializable |
51 | { // NOPMD |
|
52 | // ********************************* Fields ********************************* |
|
53 | ||
54 | // --- constants ------------------------------------------------------------ |
|
55 | ||
56 | /** |
|
57 | * The class version identifier. |
|
58 | */ |
|
59 | private static final long serialVersionUID = 1L; |
|
60 | ||
61 | /** |
|
62 | * The path to the folder within <code>META-INF</code> where properties |
|
63 | * resources are located. |
|
64 | * <p> |
|
65 | * The value of this constant is {@value}. |
|
66 | * </p> |
|
67 | */ |
|
68 | public static final String META_INF_HOME = "META-INF/smartics-properties"; |
|
69 | ||
70 | /** |
|
71 | * Pattern to detect boot properties files. |
|
72 | * |
|
73 | * @impl Note that the pattern here is only the file name pattern without a |
|
74 | * path. |
|
75 | */ |
|
76 | 0 | public static final Pattern BOOT_PROPERTIES_PATTERN = Pattern |
77 | .compile("boot\\.properties"); |
|
78 | ||
79 | /** |
|
80 | * The the path to the <code>boot.properties</code> file to access boot |
|
81 | * configuration properties to create the configuration infrastructure. |
|
82 | * <p> |
|
83 | * This file is only found by the boot definition search process, but hidden |
|
84 | * from the main definition search process. |
|
85 | * </p> |
|
86 | * <p> |
|
87 | * The value of this constant is {@value}. |
|
88 | * </p> |
|
89 | */ |
|
90 | public static final String BOOT_PROPERTIES = META_INF_HOME |
|
91 | + "/boot.properties"; |
|
92 | ||
93 | /** |
|
94 | * The path to the folder within <code>{@value #META_INF_HOME}</code> where |
|
95 | * boot properties resources are located. |
|
96 | * <p> |
|
97 | * This folder contains definition files that are hidden from the main |
|
98 | * definition search process (no properties file in META-INFO is taken into |
|
99 | * account). Third party implementations may place properties for their |
|
100 | * implementations here, if these implementations are part of the |
|
101 | * configuration infrastructure. |
|
102 | * <p> |
|
103 | * The value of this constant is {@value}. |
|
104 | * </p> |
|
105 | */ |
|
106 | public static final String BOOT_PROPERTIES_HOME = META_INF_HOME + "/boot"; |
|
107 | ||
108 | /** |
|
109 | * The name of the properties configuration file that provides information for |
|
110 | * declarations. This file provides information in archives that provide |
|
111 | * property meta data (descriptors). |
|
112 | * <p> |
|
113 | * The value of this constant is {@value}. |
|
114 | * </p> |
|
115 | */ |
|
116 | public static final String DECLARATION_FILE_NAME = "declaration.xml"; |
|
117 | ||
118 | /** |
|
119 | * The name of the properties configuration file that provides information for |
|
120 | * definitions. This file provides information in archives that provide |
|
121 | * property values. |
|
122 | * <p> |
|
123 | * The value of this constant is {@value}. |
|
124 | * </p> |
|
125 | */ |
|
126 | public static final String DEFINITION_FILE_NAME = "definition.xml"; |
|
127 | ||
128 | /** |
|
129 | * The default location of the properties declaration configuration file. |
|
130 | * <p> |
|
131 | * The value of this constant is {@value}. |
|
132 | * </p> |
|
133 | */ |
|
134 | public static final String DECLARATION_FILE = META_INF_HOME + '/' |
|
135 | + DECLARATION_FILE_NAME; |
|
136 | ||
137 | /** |
|
138 | * The default location of the properties definition configuration file. |
|
139 | * <p> |
|
140 | * The value of this constant is {@value}. |
|
141 | * </p> |
|
142 | */ |
|
143 | public static final String DEFINITION_FILE = META_INF_HOME + '/' |
|
144 | + DEFINITION_FILE_NAME; |
|
145 | ||
146 | /** |
|
147 | * The location of the property reports within the <code>META-INF</code> |
|
148 | * folder. |
|
149 | * <p> |
|
150 | * The value of this constant is {@value}. |
|
151 | * </p> |
|
152 | */ |
|
153 | private static final String META_INF_PROPERTY_REPORT = META_INF_HOME |
|
154 | + "/property-report/"; |
|
155 | ||
156 | /** |
|
157 | * The location of the property set reports within the <code>META-INF</code> |
|
158 | * folder. |
|
159 | * <p> |
|
160 | * The value of this constant is {@value}. |
|
161 | * </p> |
|
162 | */ |
|
163 | public static final String META_INF_PROPERTY_SET_REPORT = |
|
164 | META_INF_HOME + "/property-set-report/"; |
|
165 | ||
166 | /** |
|
167 | * Specifies the default report location on the home page. |
|
168 | * <p> |
|
169 | * The value of this constant is {@value}. |
|
170 | * </p> |
|
171 | */ |
|
172 | private static final String DEFAULT_REPORT_LOCATION = "/property"; |
|
173 | ||
174 | // --- members -------------------------------------------------------------- |
|
175 | ||
176 | /** |
|
177 | * The list of supported locales. The list contains locales the context |
|
178 | * provides localized information for. |
|
179 | * |
|
180 | * @serial |
|
181 | */ |
|
182 | private final List<Locale> locales; |
|
183 | ||
184 | /** |
|
185 | * The URL to the home page of the project. May be <code>null</code>. |
|
186 | * |
|
187 | * @serial |
|
188 | */ |
|
189 | private final String homePageUrl; |
|
190 | ||
191 | /** |
|
192 | * The URL to the root directory of smartics properties reports. May be |
|
193 | * <code>null</code>. |
|
194 | * |
|
195 | * @serial |
|
196 | */ |
|
197 | private final String propertiesReportUrl; |
|
198 | ||
199 | /** |
|
200 | * The mapping of alias names of property reports to their physical names. |
|
201 | * |
|
202 | * @impl Although {@link PropertyAliasMapping} is not thread-safe, this |
|
203 | * instance provides no write-access to it. |
|
204 | * @serial |
|
205 | */ |
|
206 | private final PropertyAliasMapping aliasMapping; |
|
207 | ||
208 | // ****************************** Initializer ******************************* |
|
209 | ||
210 | // ****************************** Constructors ****************************** |
|
211 | ||
212 | /** |
|
213 | * Default constructor. |
|
214 | */ |
|
215 | private PropertiesContext(final Builder builder) |
|
216 | 0 | { |
217 | 0 | this.locales = builder.locales; |
218 | 0 | this.homePageUrl = builder.homePageUrl; |
219 | 0 | this.propertiesReportUrl = builder.propertiesReportUrl; |
220 | 0 | this.aliasMapping = builder.aliasMapping; |
221 | 0 | } |
222 | ||
223 | // ****************************** Inner Classes ***************************** |
|
224 | ||
225 | /** |
|
226 | * The builder of {@link PropertiesContext}. |
|
227 | */ |
|
228 | 0 | public static final class Builder |
229 | { |
|
230 | // ******************************** Fields ******************************** |
|
231 | ||
232 | // --- constants ---------------------------------------------------------- |
|
233 | ||
234 | // --- members ------------------------------------------------------------ |
|
235 | ||
236 | /** |
|
237 | * The list of supported locales. The list contains locales the context |
|
238 | * provides localized information for. |
|
239 | */ |
|
240 | 0 | private List<Locale> locales = new ArrayList<Locale>(); |
241 | ||
242 | /** |
|
243 | * The URL to the home page of the project. |
|
244 | */ |
|
245 | private String homePageUrl; |
|
246 | ||
247 | /** |
|
248 | * The URL to the root directory of smartics properties reports. |
|
249 | */ |
|
250 | private String propertiesReportUrl; |
|
251 | ||
252 | /** |
|
253 | * The mapping of alias names of property reports to their physical names. |
|
254 | */ |
|
255 | 0 | private final PropertyAliasMapping aliasMapping = |
256 | new PropertyAliasMapping(); |
|
257 | ||
258 | // ***************************** Initializer ****************************** |
|
259 | ||
260 | // ***************************** Constructors ***************************** |
|
261 | ||
262 | // ***************************** Inner Classes **************************** |
|
263 | ||
264 | // ******************************** Methods ******************************* |
|
265 | ||
266 | // --- init --------------------------------------------------------------- |
|
267 | ||
268 | // --- get&set ------------------------------------------------------------ |
|
269 | ||
270 | /** |
|
271 | * Sets the list of supported locales. The list contains locales the context |
|
272 | * provides localized information for. |
|
273 | * |
|
274 | * @param locales the list of supported locales. |
|
275 | * @return a reference to the builder. |
|
276 | * @throws NullPointerException if {@code locales} is <code>null</code>. |
|
277 | */ |
|
278 | public Builder withLocales(final List<Locale> locales) |
|
279 | throws NullPointerException |
|
280 | { |
|
281 | 0 | this.locales = Arg.checkNotNull("locales", locales); |
282 | 0 | return this; |
283 | } |
|
284 | ||
285 | /** |
|
286 | * Sets the URL to the home page of the project. |
|
287 | * |
|
288 | * @param homePageUrl the URL to the home page of the project. |
|
289 | * @return a reference to the builder. |
|
290 | * @throws NullPointerException if {@code homePageUrl} is <code>null</code>. |
|
291 | * @throws IllegalArgumentException if {@code homePageUrl} is blank. |
|
292 | */ |
|
293 | public Builder withHomePageUrl(final String homePageUrl) |
|
294 | throws NullPointerException, IllegalArgumentException |
|
295 | { |
|
296 | 0 | this.homePageUrl = |
297 | normalizeUrl(Arg.checkNotBlank("homePageUrl", homePageUrl)); |
|
298 | 0 | return this; |
299 | } |
|
300 | ||
301 | /** |
|
302 | * Sets the URL to the root directory of smartics properties reports. |
|
303 | * |
|
304 | * @param propertiesReportUrl the URL to the root directory of smartics |
|
305 | * properties reports. |
|
306 | * @return a reference to the builder. |
|
307 | * @throws NullPointerException if {@code propertiesReportUrl} is |
|
308 | * <code>null</code>. |
|
309 | * @throws IllegalArgumentException if {@code propertiesReportUrl} is blank. |
|
310 | */ |
|
311 | public Builder withPropertiesReportUrl(final String propertiesReportUrl) |
|
312 | throws NullPointerException, IllegalArgumentException |
|
313 | { |
|
314 | 0 | Arg.checkNotBlank("propertiesReportUrl", propertiesReportUrl); |
315 | 0 | this.propertiesReportUrl = normalizeUrl(propertiesReportUrl); |
316 | 0 | return this; |
317 | } |
|
318 | ||
319 | /** |
|
320 | * Adds a new alias to the given physical resource. |
|
321 | * |
|
322 | * @param alias the new alias. |
|
323 | * @param physical the resource the alias refers to. |
|
324 | * @return a reference to the builder. |
|
325 | * @throws BlankArgumentException if either {@code alias} or |
|
326 | * {@code physical} is blank. |
|
327 | * @throws DuplicateAliasException if there is already an alias registered |
|
328 | * that points to a different physical resource. |
|
329 | */ |
|
330 | public Builder withAlias(final String alias, final String physical) |
|
331 | throws BlankArgumentException, DuplicateAliasException |
|
332 | { |
|
333 | 0 | aliasMapping.add(alias, physical); |
334 | 0 | return this; |
335 | } |
|
336 | ||
337 | // --- business ----------------------------------------------------------- |
|
338 | ||
339 | /** |
|
340 | * Creates the instance. |
|
341 | * |
|
342 | * @return the new instance. |
|
343 | */ |
|
344 | public PropertiesContext build() |
|
345 | { |
|
346 | 0 | return new PropertiesContext(this); |
347 | } |
|
348 | ||
349 | // --- object basics ------------------------------------------------------ |
|
350 | } |
|
351 | ||
352 | // ********************************* Methods ******************************** |
|
353 | ||
354 | // --- init ----------------------------------------------------------------- |
|
355 | ||
356 | // --- factory -------------------------------------------------------------- |
|
357 | ||
358 | /** |
|
359 | * Creates an empty context. |
|
360 | * |
|
361 | * @return an empty context. |
|
362 | */ |
|
363 | public static PropertiesContext createEmptyContext() |
|
364 | { |
|
365 | 0 | return new Builder().build(); |
366 | } |
|
367 | ||
368 | // --- get&set -------------------------------------------------------------- |
|
369 | ||
370 | /** |
|
371 | * Returns the URL to the home page of the project. |
|
372 | * |
|
373 | * @return the URL to the home page of the project. May be <code>null</code>. |
|
374 | */ |
|
375 | @CheckForNull |
|
376 | public String getHomePageUrl() |
|
377 | { |
|
378 | 0 | return homePageUrl; |
379 | } |
|
380 | ||
381 | /** |
|
382 | * Returns the URL to the root directory of smartics properties reports. |
|
383 | * |
|
384 | * @return the URL to the root directory of smartics properties reports. May |
|
385 | * be <code>null</code>. |
|
386 | */ |
|
387 | @CheckForNull |
|
388 | public String getPropertiesReportUrl() |
|
389 | { |
|
390 | 0 | if (propertiesReportUrl == null && homePageUrl != null) |
391 | { |
|
392 | 0 | return homePageUrl + DEFAULT_REPORT_LOCATION; |
393 | } |
|
394 | 0 | return propertiesReportUrl; |
395 | } |
|
396 | ||
397 | /** |
|
398 | * Returns the URL to the index document of smartics properties reports. |
|
399 | * |
|
400 | * @return the URL to the index document of smartics properties reports. May |
|
401 | * be <code>null</code>. |
|
402 | */ |
|
403 | @CheckForNull |
|
404 | public String getPropertiesReportIndexUrl() |
|
405 | { |
|
406 | 0 | return createReportUrl("smartics-properties-report"); |
407 | } |
|
408 | ||
409 | /** |
|
410 | * Returns the list of supported locales. The list contains locales the |
|
411 | * context provides localized information for. |
|
412 | * |
|
413 | * @return the list of supported locales. |
|
414 | */ |
|
415 | public List<Locale> getLocales() |
|
416 | { |
|
417 | 0 | return locales; |
418 | } |
|
419 | ||
420 | // --- business ------------------------------------------------------------- |
|
421 | ||
422 | private static String normalizeUrl(final String url) |
|
423 | { |
|
424 | 0 | return StringUtils.chomp(url, "/"); |
425 | } |
|
426 | ||
427 | /** |
|
428 | * Returns the URL to the relative target. |
|
429 | * |
|
430 | * @param target the relative URL. |
|
431 | * @return the absolute URL to the report or <code>null</code> if no root URL |
|
432 | * is provided by the context. |
|
433 | * @throws IllegalArgumentException of {@code target} is blank. |
|
434 | */ |
|
435 | public String createReportUrl(final String target) |
|
436 | throws IllegalArgumentException |
|
437 | { |
|
438 | 0 | Arg.checkNotBlank("target", target); |
439 | ||
440 | 0 | if (StringUtils.isEmpty(propertiesReportUrl)) |
441 | { |
|
442 | 0 | return null; |
443 | } |
|
444 | ||
445 | 0 | final String htmlFile = target + ".html"; |
446 | 0 | if (target.charAt(0) == '/') |
447 | { |
|
448 | 0 | return propertiesReportUrl + htmlFile; |
449 | } |
|
450 | else |
|
451 | { |
|
452 | 0 | return propertiesReportUrl + '/' + htmlFile; |
453 | } |
|
454 | } |
|
455 | ||
456 | /** |
|
457 | * Returns the URL to the report documentation for the given descriptor. |
|
458 | * |
|
459 | * @param descriptor the properties descriptor whose report documentation URL |
|
460 | * is requested. |
|
461 | * @return the absolute URL to the report. |
|
462 | */ |
|
463 | public String createReportUrl(final PropertyDescriptor descriptor) |
|
464 | { |
|
465 | 0 | final String target = resolve(descriptor); |
466 | 0 | final String url = createReportUrl(target); |
467 | 0 | return url; |
468 | } |
|
469 | ||
470 | private static String resolve(final PropertyDescriptor descriptor) |
|
471 | { |
|
472 | 0 | final DocumentName name = descriptor.getDocumentName(); |
473 | 0 | final String target = name.getName(); |
474 | // final String target = descriptor.getKey().toString(); |
|
475 | 0 | return target; |
476 | } |
|
477 | ||
478 | /** |
|
479 | * Returns the URL to the XML report in the META-INF directory of the given |
|
480 | * descriptor. |
|
481 | * |
|
482 | * @param descriptor the properties descriptor whose XML report URL is |
|
483 | * requested. |
|
484 | * @return the class loader root rooted path. |
|
485 | */ |
|
486 | public String createMetaInfPath(final PropertyDescriptor descriptor) |
|
487 | { |
|
488 | 0 | return createMetaInfPath(descriptor, null); |
489 | } |
|
490 | ||
491 | /** |
|
492 | * Returns the URL to the XML report in the META-INF directory of the given |
|
493 | * descriptor. |
|
494 | * |
|
495 | * @param descriptor the properties descriptor whose XML report URL is |
|
496 | * requested. |
|
497 | * @param locale the locale to determine the comments. |
|
498 | * @return the class loader root rooted path. |
|
499 | */ |
|
500 | @SuppressWarnings("unchecked") |
|
501 | public String createMetaInfPath(final PropertyDescriptor descriptor, |
|
502 | final Locale locale) |
|
503 | { |
|
504 | 0 | final String target = resolve(descriptor); |
505 | ||
506 | 0 | if (locale != null) |
507 | { |
|
508 | 0 | final List<Locale> locales = LocaleUtils.localeLookupList(locale); |
509 | 0 | for (final Locale currentLocale : locales) |
510 | { |
|
511 | 0 | final String path = |
512 | META_INF_PROPERTY_REPORT + target + '_' + currentLocale + ".xml"; |
|
513 | 0 | final ClassLoader loader = descriptor.getClass().getClassLoader(); // NOPMD |
514 | 0 | final URL resource = loader.getResource(path); |
515 | 0 | if (resource != null) |
516 | { |
|
517 | 0 | return path; |
518 | } |
|
519 | 0 | } |
520 | } |
|
521 | ||
522 | 0 | final String path = META_INF_PROPERTY_REPORT + target + ".xml"; |
523 | 0 | return path; |
524 | } |
|
525 | ||
526 | /** |
|
527 | * Returns the URL to the property set XML report in the META-INF directory of |
|
528 | * the given descriptor. |
|
529 | * |
|
530 | * @param descriptor the properties descriptor whose property set XML report |
|
531 | * URL is requested. |
|
532 | * @return the class loader root rooted path. |
|
533 | */ |
|
534 | public String createMetaInfPathPropertySet(final PropertyDescriptor descriptor) |
|
535 | { |
|
536 | 0 | return createMetaInfPathPropertySet(descriptor, null); |
537 | } |
|
538 | ||
539 | /** |
|
540 | * Returns the URL to the property set XML report in the META-INF directory of |
|
541 | * the given descriptor. |
|
542 | * |
|
543 | * @param descriptor the properties descriptor whose property set XML report |
|
544 | * URL is requested. |
|
545 | * @param locale the locale to determine the comments. |
|
546 | * @return the class loader root rooted path. |
|
547 | */ |
|
548 | @SuppressWarnings("unchecked") |
|
549 | public String createMetaInfPathPropertySet( |
|
550 | final PropertyDescriptor descriptor, final Locale locale) |
|
551 | { |
|
552 | 0 | final String target = descriptor.getKey().getPropertySet(); |
553 | ||
554 | 0 | if (locale != null) |
555 | { |
|
556 | 0 | final List<Locale> locales = LocaleUtils.localeLookupList(locale); |
557 | 0 | for (final Locale currentLocale : locales) |
558 | { |
|
559 | 0 | final String path = |
560 | META_INF_PROPERTY_SET_REPORT + target + '_' + currentLocale |
|
561 | + ".xml"; |
|
562 | 0 | final ClassLoader loader = descriptor.getClass().getClassLoader(); // NOPMD |
563 | 0 | final URL resource = loader.getResource(path); |
564 | 0 | if (resource != null) |
565 | { |
|
566 | 0 | return path; |
567 | } |
|
568 | 0 | } |
569 | } |
|
570 | ||
571 | 0 | final String path = META_INF_PROPERTY_SET_REPORT + target + ".xml"; |
572 | 0 | return path; |
573 | } |
|
574 | ||
575 | /** |
|
576 | * Resolves the alias to the target it points to. |
|
577 | * |
|
578 | * @param alias the alias whose physical resource is requested. |
|
579 | * @return the target the alias points to. |
|
580 | * @throws BlankArgumentException if {@code alias} is blank. |
|
581 | * @throws UnknownAliasException if the alias is not known. |
|
582 | */ |
|
583 | public String resolve(final String alias) throws BlankArgumentException, |
|
584 | UnknownAliasException |
|
585 | { |
|
586 | 0 | return aliasMapping.get(alias); |
587 | } |
|
588 | ||
589 | /** |
|
590 | * Traverses the registered aliases. |
|
591 | * |
|
592 | * @param traverser the traverser to use. |
|
593 | * @throws NullPointerException if {@code traverser} is <code>null</code>. |
|
594 | */ |
|
595 | public void traverseAliases(final AliasTraverser traverser) |
|
596 | throws NullPointerException |
|
597 | { |
|
598 | 0 | aliasMapping.traverse(traverser); |
599 | 0 | } |
600 | ||
601 | /** |
|
602 | * Checks whether any aliases are registered. |
|
603 | * |
|
604 | * @return <code>true</code> if at least one alias is registered, |
|
605 | * <code>false</code> otherwise. |
|
606 | */ |
|
607 | public boolean hasAliases() |
|
608 | { |
|
609 | 0 | return !aliasMapping.isEmpty(); |
610 | } |
|
611 | ||
612 | // --- object basics -------------------------------------------------------- |
|
613 | ||
614 | /** |
|
615 | * Returns the string representation of the object. |
|
616 | * |
|
617 | * @return the string representation of the object. |
|
618 | */ |
|
619 | @Override |
|
620 | public String toString() |
|
621 | { |
|
622 | 0 | return ToStringBuilder.reflectionToString(this); |
623 | } |
|
624 | } |