Line 0
Link Here
|
|
|
1 |
/* |
2 |
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. |
3 |
* |
4 |
* Copyright 2008 Sun Microsystems, Inc. All rights reserved. |
5 |
* |
6 |
* The contents of this file are subject to the terms of either the GNU |
7 |
* General Public License Version 2 only ("GPL") or the Common |
8 |
* Development and Distribution License("CDDL") (collectively, the |
9 |
* "License"). You may not use this file except in compliance with the |
10 |
* License. You can obtain a copy of the License at |
11 |
* http://www.netbeans.org/cddl-gplv2.html |
12 |
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the |
13 |
* specific language governing permissions and limitations under the |
14 |
* License. When distributing the software, include this License Header |
15 |
* Notice in each file and include the License file at |
16 |
* nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this |
17 |
* particular file as subject to the "Classpath" exception as provided |
18 |
* by Sun in the GPL Version 2 section of the License file that |
19 |
* accompanied this code. If applicable, add the following below the |
20 |
* License Header, with the fields enclosed by brackets [] replaced by |
21 |
* your own identifying information: |
22 |
* "Portions Copyrighted [year] [name of copyright owner]" |
23 |
* |
24 |
* If you wish your version of this file to be governed by only the CDDL |
25 |
* or only the GPL Version 2, indicate your decision by adding |
26 |
* "[Contributor] elects to include this software in this distribution |
27 |
* under the [CDDL or GPL Version 2] license." If you do not indicate a |
28 |
* single choice of license, a recipient has the option to distribute |
29 |
* your version of this file under either the CDDL, the GPL Version 2 or |
30 |
* to extend the choice of license to its licensees as provided above. |
31 |
* However, if you add GPL Version 2 code and therefore, elected the GPL |
32 |
* Version 2 license, then the option applies only if the new code is |
33 |
* made subject to such option by the copyright holder. |
34 |
* |
35 |
* Contributor(s): |
36 |
* |
37 |
* Portions Copyrighted 2008 Sun Microsystems, Inc. |
38 |
*/ |
39 |
|
40 |
package org.openide.filesystems.annotations; |
41 |
|
42 |
import java.io.IOException; |
43 |
import java.io.InputStream; |
44 |
import java.net.URL; |
45 |
import java.util.Arrays; |
46 |
import java.util.LinkedHashMap; |
47 |
import java.util.Locale; |
48 |
import java.util.Map; |
49 |
import java.util.Properties; |
50 |
import java.util.regex.Matcher; |
51 |
import java.util.regex.Pattern; |
52 |
import javax.annotation.processing.Filer; |
53 |
import javax.annotation.processing.ProcessingEnvironment; |
54 |
import javax.lang.model.element.ElementKind; |
55 |
import javax.lang.model.element.ExecutableElement; |
56 |
import javax.lang.model.element.Modifier; |
57 |
import javax.lang.model.element.PackageElement; |
58 |
import javax.lang.model.element.TypeElement; |
59 |
import javax.lang.model.type.TypeMirror; |
60 |
import javax.lang.model.util.ElementFilter; |
61 |
import javax.tools.Diagnostic.Kind; |
62 |
import javax.tools.StandardLocation; |
63 |
import org.w3c.dom.Document; |
64 |
import org.w3c.dom.Element; |
65 |
import org.w3c.dom.NodeList; |
66 |
|
67 |
/** |
68 |
* Convenience class for generating fragments of an XML layer. |
69 |
* @see LayerGeneratingProcessor#layer |
70 |
* @since XXX #150447 |
71 |
*/ |
72 |
public final class LayerBuilder { |
73 |
|
74 |
private final Document doc; |
75 |
|
76 |
/** |
77 |
* Creates a new builder. |
78 |
* @param document a DOM representation of an XML layer which will be modified |
79 |
*/ |
80 |
public LayerBuilder(Document document) { |
81 |
this.doc = document; |
82 |
} |
83 |
|
84 |
/** |
85 |
* Adds a file to the layer. |
86 |
* You need to {@link File#write} it in order to finalize the effect. |
87 |
* @param path the full path to the desired file in resource format, e.g. {@code "Menu/File/exit.instance"} |
88 |
* @return a file builder |
89 |
*/ |
90 |
public File file(String path) { |
91 |
return new File(path); |
92 |
} |
93 |
|
94 |
/** |
95 |
* Generates an instance file whose {@code InstanceCookie} would load a given class or method. |
96 |
* Useful for {@link LayerGeneratingProcessor}s which define layer fragments which instantiate Java objects from the annotated code. |
97 |
* <p>While you can pick a specific instance file name, if possible you should pass null for {@code name} |
98 |
* as using the generated name will help avoid accidental name collisions between annotations. |
99 |
* @param annotationTarget an annotated {@linkplain TypeElement class} or {@linkplain ExecutableElement method} |
100 |
* @param path path to folder of instance file, e.g. {@code "Menu/File"} |
101 |
* @param name instance file basename, e.g. {@code "my-menu-Item"}, or null to pick a name according to the element |
102 |
* @param type a type to which the instance ought to be assignable, or null to skip this check |
103 |
* @param processingEnv a processor environment used for {@link ProcessingEnvironment#getElementUtils} and {@link ProcessingEnvironment#getTypeUtils} |
104 |
* @return an instance file (call {@link File#write} to finalize) |
105 |
* @throws IllegalArgumentException if the annotationTarget is not of a suitable sort |
106 |
* (detail message can be reported as a {@link Kind#ERROR}) |
107 |
*/ |
108 |
public File instanceFile(javax.lang.model.element.Element annotationTarget, String path, String name, Class type, |
109 |
ProcessingEnvironment processingEnv) throws IllegalArgumentException { |
110 |
String[] clazzOrMethod = instantiableClassOrMethod(annotationTarget, type, processingEnv); |
111 |
String clazz = clazzOrMethod[0]; |
112 |
String method = clazzOrMethod[1]; |
113 |
String basename; |
114 |
if (name == null) { |
115 |
basename = clazz.replace('.', '-'); |
116 |
if (method != null) { |
117 |
basename += "-" + method; |
118 |
} |
119 |
} else { |
120 |
basename = name; |
121 |
} |
122 |
LayerBuilder.File f = file(path + "/" + basename + ".instance"); |
123 |
if (method != null) { |
124 |
f.methodvalue("instanceCreate", clazz, method); |
125 |
} else if (name != null) { |
126 |
f.stringvalue("instanceClass", clazz); |
127 |
} // else name alone suffices |
128 |
return f; |
129 |
} |
130 |
|
131 |
private static String[] instantiableClassOrMethod(javax.lang.model.element.Element annotationTarget, Class type, |
132 |
ProcessingEnvironment processingEnv) throws IllegalArgumentException { |
133 |
TypeMirror typeMirror = type != null ? processingEnv.getElementUtils().getTypeElement(type.getName().replace('$', '.')).asType() : null; |
134 |
switch (annotationTarget.getKind()) { |
135 |
case CLASS: { |
136 |
String clazz = processingEnv.getElementUtils().getBinaryName((TypeElement) annotationTarget).toString(); |
137 |
if (annotationTarget.getModifiers().contains(Modifier.ABSTRACT)) { |
138 |
throw new IllegalArgumentException(clazz + " must not be abstract"); |
139 |
} |
140 |
{ |
141 |
boolean hasDefaultCtor = false; |
142 |
for (ExecutableElement constructor : ElementFilter.constructorsIn(annotationTarget.getEnclosedElements())) { |
143 |
if (constructor.getParameters().isEmpty()) { |
144 |
hasDefaultCtor = true; |
145 |
break; |
146 |
} |
147 |
} |
148 |
if (!hasDefaultCtor) { |
149 |
throw new IllegalArgumentException(clazz + " must have a no-argument constructor"); |
150 |
} |
151 |
} |
152 |
if (typeMirror != null && !processingEnv.getTypeUtils().isAssignable(annotationTarget.asType(), typeMirror)) { |
153 |
throw new IllegalArgumentException(clazz + " is not assignable to " + typeMirror); |
154 |
} |
155 |
return new String[] {clazz, null}; |
156 |
} |
157 |
case METHOD: { |
158 |
String clazz = processingEnv.getElementUtils().getBinaryName((TypeElement) annotationTarget.getEnclosingElement()).toString(); |
159 |
String method = annotationTarget.getSimpleName().toString(); |
160 |
if (!annotationTarget.getModifiers().contains(Modifier.STATIC)) { |
161 |
throw new IllegalArgumentException(clazz + "." + method + " must be static"); |
162 |
} |
163 |
if (!((ExecutableElement) annotationTarget).getParameters().isEmpty()) { |
164 |
throw new IllegalArgumentException(clazz + "." + method + " must not take arguments"); |
165 |
} |
166 |
if (typeMirror != null && !processingEnv.getTypeUtils().isAssignable(((ExecutableElement) annotationTarget).getReturnType(), typeMirror)) { |
167 |
throw new IllegalArgumentException(clazz + "." + method + " is not assignable to " + typeMirror); |
168 |
} |
169 |
return new String[] {clazz, method}; |
170 |
} |
171 |
default: |
172 |
throw new IllegalArgumentException("Annotated element is not loadable as an instance: " + annotationTarget); |
173 |
} |
174 |
} |
175 |
|
176 |
/** |
177 |
* Convenience method to create a shadow file (like a symbolic link). |
178 |
* <p>While you can pick a specific shadow file name, if possible you should pass null for {@code name} |
179 |
* as using the generated name will help avoid accidental name collisions between annotations. |
180 |
* @param target the complete path to the original file (use {@link File#getPath} if you just made it) |
181 |
* @param folder the folder path in which to create the shadow, e.g. {@code "Menu/File"} |
182 |
* @param name the basename of the shadow file sans extension, e.g. {@code "my-Action"}, or null to pick a default |
183 |
* @return a shadow file (call {@link File#write} to finalize) |
184 |
*/ |
185 |
public File shadowFile(String target, String folder, String name) { |
186 |
if (name == null) { |
187 |
name = target.replaceFirst("^.+/", "").replaceFirst("\\.[^./]+$", ""); |
188 |
} |
189 |
return file(folder + "/" + name + ".shadow").stringvalue("originalFile", target); |
190 |
} |
191 |
|
192 |
/** |
193 |
* Builder for creating a single file entry. |
194 |
*/ |
195 |
public final class File { |
196 |
|
197 |
private final String path; |
198 |
private final Map<String,String[]> attrs = new LinkedHashMap<String,String[]>(); |
199 |
private String contents; |
200 |
private String url; |
201 |
|
202 |
File(String path) { |
203 |
this.path = path; |
204 |
} |
205 |
|
206 |
/** |
207 |
* Gets the path this file is to be created under. |
208 |
* @return the configured path, as in {@link #file} |
209 |
*/ |
210 |
public String getPath() { |
211 |
return path; |
212 |
} |
213 |
|
214 |
/** |
215 |
* Configures the file to have inline text contents. |
216 |
* @param contents text to use as the body of the file |
217 |
* @return this builder |
218 |
*/ |
219 |
public File contents(String contents) { |
220 |
if (this.contents != null || url != null || contents == null) { |
221 |
throw new IllegalArgumentException(); |
222 |
} |
223 |
this.contents = contents; |
224 |
return this; |
225 |
} |
226 |
|
227 |
/** |
228 |
* Configures the file to have external contents. |
229 |
* @param url a URL to the body of the file, e.g. {@code "nbresloc:/org/my/module/resources/definition.xml"} |
230 |
* or more commonly an absolute resource path such as {@code "/org/my/module/resources/definition.xml"} |
231 |
* @return this builder |
232 |
*/ |
233 |
public File url(String url) { |
234 |
if (contents != null || this.url != null || url == null) { |
235 |
throw new IllegalArgumentException(); |
236 |
} |
237 |
this.url = url; |
238 |
return this; |
239 |
} |
240 |
|
241 |
/** |
242 |
* Adds a string-valued attribute. |
243 |
* @param attr the attribute name |
244 |
* @param value the attribute value |
245 |
* @return this builder |
246 |
*/ |
247 |
public File stringvalue(String attr, String value) { |
248 |
attrs.put(attr, new String[] {"stringvalue", value}); |
249 |
return this; |
250 |
} |
251 |
|
252 |
/** |
253 |
* Adds a byte-valued attribute. |
254 |
* @param attr the attribute name |
255 |
* @param value the attribute value |
256 |
* @return this builder |
257 |
*/ |
258 |
public File bytevalue(String attr, byte value) { |
259 |
attrs.put(attr, new String[] {"bytevalue", Byte.toString(value)}); |
260 |
return this; |
261 |
} |
262 |
|
263 |
/** |
264 |
* Adds a short-valued attribute. |
265 |
* @param attr the attribute name |
266 |
* @param value the attribute value |
267 |
* @return this builder |
268 |
*/ |
269 |
public File shortvalue(String attr, short value) { |
270 |
attrs.put(attr, new String[] {"shortvalue", Short.toString(value)}); |
271 |
return this; |
272 |
} |
273 |
|
274 |
/** |
275 |
* Adds an int-valued attribute. |
276 |
* @param attr the attribute name |
277 |
* @param value the attribute value |
278 |
* @return this builder |
279 |
*/ |
280 |
public File intvalue(String attr, int value) { |
281 |
attrs.put(attr, new String[] {"intvalue", Integer.toString(value)}); |
282 |
return this; |
283 |
} |
284 |
|
285 |
/** |
286 |
* Adds a long-valued attribute. |
287 |
* @param attr the attribute name |
288 |
* @param value the attribute value |
289 |
* @return this builder |
290 |
*/ |
291 |
public File longvalue(String attr, long value) { |
292 |
attrs.put(attr, new String[] {"longvalue", Long.toString(value)}); |
293 |
return this; |
294 |
} |
295 |
|
296 |
/** |
297 |
* Adds a float-valued attribute. |
298 |
* @param attr the attribute name |
299 |
* @param value the attribute value |
300 |
* @return this builder |
301 |
*/ |
302 |
public File floatvalue(String attr, float value) { |
303 |
attrs.put(attr, new String[] {"floatvalue", Float.toString(value)}); |
304 |
return this; |
305 |
} |
306 |
|
307 |
/** |
308 |
* Adds a double-valued attribute. |
309 |
* @param attr the attribute name |
310 |
* @param value the attribute value |
311 |
* @return this builder |
312 |
*/ |
313 |
public File doublevalue(String attr, double value) { |
314 |
attrs.put(attr, new String[] {"doublevalue", Double.toString(value)}); |
315 |
return this; |
316 |
} |
317 |
|
318 |
/** |
319 |
* Adds a boolean-valued attribute. |
320 |
* @param attr the attribute name |
321 |
* @param value the attribute value |
322 |
* @return this builder |
323 |
*/ |
324 |
public File boolvalue(String attr, boolean value) { |
325 |
attrs.put(attr, new String[] {"boolvalue", Boolean.toString(value)}); |
326 |
return this; |
327 |
} |
328 |
|
329 |
/** |
330 |
* Adds a character-valued attribute. |
331 |
* @param attr the attribute name |
332 |
* @param value the attribute value |
333 |
* @return this builder |
334 |
*/ |
335 |
public File charvalue(String attr, char value) { |
336 |
attrs.put(attr, new String[] {"charvalue", Character.toString(value)}); |
337 |
return this; |
338 |
} |
339 |
|
340 |
/** |
341 |
* Adds a URL-valued attribute. |
342 |
* @param attr the attribute name |
343 |
* @param value the attribute value |
344 |
* @return this builder |
345 |
*/ |
346 |
public File urlvalue(String attr, URL value) { |
347 |
attrs.put(attr, new String[] {"urlvalue", value.toString()}); |
348 |
return this; |
349 |
} |
350 |
|
351 |
/** |
352 |
* Adds an attribute loaded from a Java method. |
353 |
* @param attr the attribute name |
354 |
* @param clazz the fully-qualified name of the factory class |
355 |
* @param method the name of a static method |
356 |
* @return this builder |
357 |
*/ |
358 |
public File methodvalue(String attr, String clazz, String method) { |
359 |
attrs.put(attr, new String[] {"methodvalue", clazz + "." + method}); |
360 |
return this; |
361 |
} |
362 |
|
363 |
/** |
364 |
* Adds an attribute loaded from a Java constructor. |
365 |
* @param attr the attribute name |
366 |
* @param clazz the fully-qualified name of a class with a no-argument constructor |
367 |
* @return this builder |
368 |
*/ |
369 |
public File newvalue(String attr, String clazz) { |
370 |
attrs.put(attr, new String[] {"newvalue", clazz}); |
371 |
return this; |
372 |
} |
373 |
|
374 |
/** |
375 |
* Adds an attribute to load a given class or method. |
376 |
* Useful for {@link LayerGeneratingProcessor}s which define layer fragments which instantiate Java objects from the annotated code. |
377 |
* @param attr the attribute name |
378 |
* @param annotationTarget an annotated {@linkplain TypeElement class} or {@linkplain ExecutableElement method} |
379 |
* @param type a type to which the instance ought to be assignable, or null to skip this check |
380 |
* @param processingEnv a processor environment used for {@link ProcessingEnvironment#getElementUtils} and {@link ProcessingEnvironment#getTypeUtils} |
381 |
* @return this builder |
382 |
* @throws IllegalArgumentException if the annotationTarget is not of a suitable sort |
383 |
* (detail message can be reported as a {@link Kind#ERROR}) |
384 |
*/ |
385 |
public File instanceAttribute(String attr, javax.lang.model.element.Element annotationTarget, Class type, |
386 |
ProcessingEnvironment processingEnv) throws IllegalArgumentException { |
387 |
String[] clazzOrMethod = instantiableClassOrMethod(annotationTarget, type, processingEnv); |
388 |
if (clazzOrMethod[1] == null) { |
389 |
newvalue(attr, clazzOrMethod[0]); |
390 |
} else { |
391 |
methodvalue(attr, clazzOrMethod[0], clazzOrMethod[1]); |
392 |
} |
393 |
return this; |
394 |
} |
395 |
|
396 |
/** |
397 |
* Adds an attribute loaded from a resource bundle. |
398 |
* @param attr the attribute name |
399 |
* @param bundle the full name of the bundle, e.g. {@code "org.my.module.Bundle"} |
400 |
* @param key the key to look up inside the bundle |
401 |
* @return this builder |
402 |
*/ |
403 |
public File bundlevalue(String attr, String bundle, String key) { |
404 |
attrs.put(attr, new String[] {"bundlevalue", bundle + "#" + key}); |
405 |
return this; |
406 |
} |
407 |
|
408 |
/** |
409 |
* Adds an attribute for a possibly localized string. |
410 |
* @param attr the attribute name |
411 |
* @param label either a general string to store as is, or a resource bundle reference |
412 |
* such as {@code "my.module.Bundle#some_key"}, |
413 |
* or just {@code "#some_key"} to load from a {@code "Bundle"} in the same package |
414 |
* @param referenceElement if not null, a source element to determine the package |
415 |
* @param filer if not null, a way to look up the source bundle to verify that it exists and has the specified key |
416 |
* @return this builder |
417 |
* @throws IllegalArgumentException if a bundle key is requested but it cannot be found in sources |
418 |
* (detail message can be reported as a {@link Kind#ERROR}) |
419 |
*/ |
420 |
public File bundlevalue(String attr, String label, javax.lang.model.element.Element referenceElement, Filer filer) throws IllegalArgumentException { |
421 |
String javaIdentifier = "(?:\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)"; |
422 |
Matcher m = Pattern.compile("((?:" + javaIdentifier + "\\.)+[^\\s.#]+)?#(\\S+)").matcher(label); |
423 |
if (m.matches()) { |
424 |
String bundle = m.group(1); |
425 |
String key = m.group(2); |
426 |
if (bundle == null) { |
427 |
while (referenceElement != null && referenceElement.getKind() != ElementKind.PACKAGE) { |
428 |
referenceElement = referenceElement.getEnclosingElement(); |
429 |
} |
430 |
if (referenceElement == null) { |
431 |
throw new IllegalArgumentException("No reference element to determine package in '" + label + "'"); |
432 |
} |
433 |
bundle = ((PackageElement) referenceElement).getQualifiedName() + ".Bundle"; |
434 |
} |
435 |
if (filer != null) { |
436 |
try { |
437 |
InputStream is = filer.getResource(StandardLocation.SOURCE_PATH, "", bundle.replace('.', '/') + ".properties").openInputStream(); |
438 |
try { |
439 |
Properties p = new Properties(); |
440 |
p.load(is); |
441 |
if (p.getProperty(key) == null) { |
442 |
throw new IllegalArgumentException("No key '" + key + "' found in " + bundle); |
443 |
} |
444 |
} finally { |
445 |
is.close(); |
446 |
} |
447 |
} catch (IOException x) { |
448 |
throw new IllegalArgumentException("Could not open " + bundle + ": " + x); |
449 |
} |
450 |
} |
451 |
bundlevalue(attr, bundle, key); |
452 |
} else { |
453 |
stringvalue(attr, label); |
454 |
} |
455 |
return this; |
456 |
} |
457 |
|
458 |
/** |
459 |
* Adds an attribute which deserializes a Java value. |
460 |
* @param attr the attribute name |
461 |
* @param data the serial data as created by {@link ObjectOutputStream} |
462 |
* @return this builder |
463 |
*/ |
464 |
public File serialvalue(String attr, byte[] data) { |
465 |
StringBuilder buf = new StringBuilder(data.length * 2); |
466 |
for (byte b : data) { |
467 |
if (b >= 0 && b < 16) { |
468 |
buf.append('0'); |
469 |
} |
470 |
buf.append(Integer.toHexString(b < 0 ? b + 256 : b)); |
471 |
} |
472 |
attrs.put(attr, new String[] {"serialvalue", buf.toString().toUpperCase(Locale.ENGLISH)}); |
473 |
return this; |
474 |
} |
475 |
|
476 |
/** |
477 |
* Sets a position attribute. |
478 |
* This is a convenience method so you can define in your annotation: |
479 |
* <code>int position() default Integer.MAX_VALUE;</code> |
480 |
* and later call: |
481 |
* <code>fileBuilder.position(annotation.position())</code> |
482 |
* @param position a numeric position for this file, or {@link Integer#MAX_VALUE} to not define any position |
483 |
* @return this builder |
484 |
*/ |
485 |
public File position(int position) { |
486 |
if (position != Integer.MAX_VALUE) { |
487 |
intvalue("position", position); |
488 |
} |
489 |
return this; |
490 |
} |
491 |
|
492 |
/** |
493 |
* Writes the file to the layer. |
494 |
* Any intervening parent folders are created automatically. |
495 |
* If the file already exists, the old copy is replaced. |
496 |
* @return the originating layer builder, in case you want to add another file |
497 |
*/ |
498 |
public LayerBuilder write() { |
499 |
Element e = doc.getDocumentElement(); |
500 |
String[] pieces = path.split("/"); |
501 |
for (String piece : Arrays.asList(pieces).subList(0, pieces.length - 1)) { |
502 |
Element kid = find(e, piece); |
503 |
if (kid != null) { |
504 |
if (!kid.getNodeName().equals("folder")) { |
505 |
throw new IllegalArgumentException(path); |
506 |
} |
507 |
e = kid; |
508 |
} else { |
509 |
e = (Element) e.appendChild(doc.createElement("folder")); |
510 |
e.setAttribute("name", piece); |
511 |
} |
512 |
} |
513 |
String piece = pieces[pieces.length - 1]; |
514 |
Element file = find(e,piece); |
515 |
if (file != null) { |
516 |
e.removeChild(file); |
517 |
} |
518 |
file = (Element) e.appendChild(doc.createElement("file")); |
519 |
file.setAttribute("name", piece); |
520 |
for (Map.Entry<String,String[]> entry : attrs.entrySet()) { |
521 |
Element attr = (Element) file.appendChild(doc.createElement("attr")); |
522 |
attr.setAttribute("name", entry.getKey()); |
523 |
attr.setAttribute(entry.getValue()[0], entry.getValue()[1]); |
524 |
} |
525 |
if (url != null) { |
526 |
file.setAttribute("url", url); |
527 |
} else if (contents != null) { |
528 |
file.appendChild(doc.createCDATASection(contents)); |
529 |
} |
530 |
return LayerBuilder.this; |
531 |
} |
532 |
|
533 |
private Element find(Element parent, String name) { |
534 |
NodeList nl = parent.getElementsByTagName("*"); |
535 |
for (int i = 0; i < nl.getLength(); i++) { |
536 |
Element e = (Element) nl.item(i); |
537 |
if (e.getAttribute("name").equals(name)) { |
538 |
return e; |
539 |
} |
540 |
} |
541 |
return null; |
542 |
} |
543 |
|
544 |
} |
545 |
|
546 |
} |