001: /*
002: * Spoon - http://spoon.gforge.inria.fr/
003: * Copyright (C) 2006 INRIA Futurs <renaud.pawlak@inria.fr>
004: *
005: * This software is governed by the CeCILL-C License under French law and
006: * abiding by the rules of distribution of free software. You can use, modify
007: * and/or redistribute the software under the terms of the CeCILL-C license as
008: * circulated by CEA, CNRS and INRIA at http://www.cecill.info.
009: *
010: * This program is distributed in the hope that it will be useful, but WITHOUT
011: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
012: * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details.
013: *
014: * The fact that you are presently reading this means that you have had
015: * knowledge of the CeCILL-C license and that you accept its terms.
016: */
017:
018: package spoon.support.template;
019:
020: import java.util.ArrayList;
021: import java.util.Collection;
022: import java.util.List;
023: import java.util.TreeSet;
024:
025: import spoon.reflect.Factory;
026: import spoon.reflect.code.CtAbstractInvocation;
027: import spoon.reflect.code.CtArrayAccess;
028: import spoon.reflect.code.CtBlock;
029: import spoon.reflect.code.CtCodeElement;
030: import spoon.reflect.code.CtExpression;
031: import spoon.reflect.code.CtFieldAccess;
032: import spoon.reflect.code.CtForEach;
033: import spoon.reflect.code.CtInvocation;
034: import spoon.reflect.code.CtLiteral;
035: import spoon.reflect.code.CtReturn;
036: import spoon.reflect.code.CtStatement;
037: import spoon.reflect.code.CtStatementList;
038: import spoon.reflect.code.CtVariableAccess;
039: import spoon.reflect.declaration.CtAnnotation;
040: import spoon.reflect.declaration.CtClass;
041: import spoon.reflect.declaration.CtConstructor;
042: import spoon.reflect.declaration.CtElement;
043: import spoon.reflect.declaration.CtExecutable;
044: import spoon.reflect.declaration.CtField;
045: import spoon.reflect.declaration.CtMethod;
046: import spoon.reflect.declaration.CtNamedElement;
047: import spoon.reflect.declaration.CtParameter;
048: import spoon.reflect.declaration.CtSimpleType;
049: import spoon.reflect.declaration.CtTypedElement;
050: import spoon.reflect.reference.CtArrayTypeReference;
051: import spoon.reflect.reference.CtExecutableReference;
052: import spoon.reflect.reference.CtFieldReference;
053: import spoon.reflect.reference.CtReference;
054: import spoon.reflect.reference.CtTypeReference;
055: import spoon.reflect.visitor.CtInheritanceScanner;
056: import spoon.reflect.visitor.CtScanner;
057: import spoon.reflect.visitor.Query;
058: import spoon.reflect.visitor.filter.VariableAccessFilter;
059: import spoon.template.Local;
060: import spoon.template.Parameter;
061: import spoon.template.Template;
062: import spoon.template.TemplateParameter;
063:
064: class SkipException extends RuntimeException {
065: private static final long serialVersionUID = 1L;
066:
067: Object skipped;
068:
069: public SkipException(Object e) {
070: super ("skipping " + e.toString());
071: skipped = e;
072: }
073:
074: }
075:
076: /**
077: * This visitor implements the substitution engine of Spoon templates.
078: */
079: public class SubstitutionVisitor extends CtScanner {
080:
081: public class InheritanceSustitutionScanner extends
082: CtInheritanceScanner {
083:
084: SubstitutionVisitor parent = null;
085:
086: public InheritanceSustitutionScanner(SubstitutionVisitor parent) {
087: this .parent = parent;
088: }
089:
090: /**
091: * Replaces method parameters when defined as a list of
092: * {@link CtParameter}.
093: */
094: @Override
095: public <R> void scanCtExecutable(CtExecutable<R> e) {
096: // replace method parameters
097: for (CtParameter<?> parameter : new ArrayList<CtParameter<?>>(
098: e.getParameters())) {
099: String name = parameter.getSimpleName();
100: for (String pname : parameterNames) {
101: if (name.equals(pname)) {
102: Object value = Parameters.getValue(template,
103: pname, null);
104: int i = parameter.getParent().getParameters()
105: .indexOf(parameter);
106: if (value instanceof List) {
107: List<?> l = (List<?>) value;
108: for (Object p : l) {
109: CtParameter<?> p2 = e.getFactory()
110: .Core().clone(
111: (CtParameter<?>) p);
112: p2.setParent(parameter.getParent());
113: parameter.getParent().getParameters()
114: .add(i++, p2);
115: }
116: parameter.getParent().getParameters()
117: .remove(parameter);
118: }
119: }
120: }
121: }
122: super .scanCtExecutable(e);
123: }
124:
125: /**
126: * Remove template-specific {@link Local} annotations.
127: */
128: @Override
129: public void scanCtElement(CtElement e) {
130: CtAnnotation<?> a = e.getAnnotation(e.getFactory().Type()
131: .createReference(Local.class));
132: if (a != null) {
133: e.getAnnotations().remove(a);
134: }
135: super .scanCtElement(e);
136: }
137:
138: /**
139: * Replaces parameters in element names (even if detected as a
140: * substring).
141: */
142: @Override
143: public void scanCtNamedElement(CtNamedElement element) {
144: if (element.getDocComment() != null) {
145: element.setDocComment(substituteInDocComment(element
146: .getDocComment()));
147: }
148: // replace parameters in names
149: String name = element.getSimpleName();
150: for (String pname : parameterNames) {
151: if (name.contains(pname)) {
152: Object value = Parameters.getValue(template, pname,
153: null);
154: if (value instanceof String) {
155: // replace with the string value
156: name = name.replace(pname, (String) value);
157: element.setSimpleName(name);
158: } else if ((value instanceof CtTypeReference)
159: && (element instanceof CtSimpleType)) {
160: // replace with the type reference's name
161: name = name.replace(pname,
162: ((CtTypeReference<?>) value)
163: .getSimpleName());
164: element.setSimpleName(name);
165: }
166: }
167: }
168: super .scanCtNamedElement(element);
169: }
170:
171: private String substituteInDocComment(String docComment) {
172: String result = docComment;
173: for (String pname : parameterNames) {
174: Object value = Parameters.getValue(template, pname,
175: null);
176: if (value instanceof String) {
177: result = result.replace(pname, (String) value);
178: }
179: }
180: return result;
181: }
182:
183: /**
184: * Removes all the elements that are part of the template definitions.
185: *
186: * @see Template
187: * @see TemplateParameter
188: * @see Local
189: * @see Parameter
190: */
191: @Override
192: public <T> void visitCtClass(CtClass<T> ctClass) {
193: ctClass.getSuperInterfaces().remove(
194: f.Type().createReference(Template.class));
195: for (CtMethod<?> m : new TreeSet<CtMethod<?>>(ctClass
196: .getMethods())) {
197: if (m.getAnnotation(Local.class) != null) {
198: ctClass.getMethods().remove(m);
199: }
200: }
201: for (CtConstructor<?> c : new TreeSet<CtConstructor<?>>(
202: ctClass.getConstructors())) {
203: if (c.getAnnotation(Local.class) != null) {
204: ctClass.getConstructors().remove(c);
205: }
206: }
207: for (CtField<?> field : new TreeSet<CtField<?>>(ctClass
208: .getFields())) {
209: if ((field.getAnnotation(Local.class) != null)
210: || Parameters.isParameterSource(field
211: .getReference())) {
212: ctClass.getFields().remove(field);
213: continue;
214: }
215: // replace fields parameters
216: String name = field.getSimpleName();
217: for (String pname : parameterNames) {
218: if (name.equals(pname)) {
219: Object value = Parameters.getValue(template,
220: pname, null);
221: int i = ctClass.getFields().indexOf(field);
222: if (value instanceof List) {
223: List<?> l = (List<?>) value;
224: for (Object f : l) {
225: CtField<?> f2 = ctClass.getFactory()
226: .Core().clone((CtField<?>) f);
227: f2.setParent(ctClass);
228: ctClass.getFields().add(i++, f2);
229: }
230: ctClass.getFields().remove(field);
231: }
232: }
233: }
234: }
235: super .visitCtClass(ctClass);
236: }
237:
238: @Override
239: public void visitCtForEach(CtForEach foreach) {
240: if (foreach.getExpression() instanceof CtFieldAccess) {
241: CtFieldAccess<?> fa = (CtFieldAccess<?>) foreach
242: .getExpression();
243: if (Parameters.isParameterSource(fa.getVariable())) {
244: Object[] value = (Object[]) Parameters.getValue(
245: template, fa.getVariable().getSimpleName(),
246: null);
247: CtStatementList<?> l = foreach.getFactory().Core()
248: .createStatementList();
249: CtStatement body = foreach.getBody();
250: for (Object element : value) {
251: CtStatement b = foreach.getFactory().Core()
252: .clone(body);
253: for (CtVariableAccess<?> va : Query
254: .getElements(
255: b,
256: new VariableAccessFilter(
257: foreach.getVariable()
258: .getReference()))) {
259: va.replace((CtElement) element);
260: }
261: l.getStatements().add(b);
262: }
263: foreach.replace(l);
264: throw new SkipException(foreach);
265: }
266: }
267: super .visitCtForEach(foreach);
268: }
269:
270: /**
271: * Replaces direct field parameter accesses.
272: */
273: @SuppressWarnings("unchecked")
274: @Override
275: public <T> void visitCtFieldAccess(CtFieldAccess<T> fieldAccess) {
276: CtFieldReference<?> ref = fieldAccess.getVariable();
277: if ("length".equals(ref.getSimpleName())) {
278: if (fieldAccess.getTarget() instanceof CtFieldAccess) {
279: ref = ((CtFieldAccess) fieldAccess.getTarget())
280: .getVariable();
281: if (Parameters.isParameterSource(ref)) {
282: Object[] value = (Object[]) Parameters
283: .getValue(template,
284: ref.getSimpleName(), null);
285: fieldAccess.replace(fieldAccess.getFactory()
286: .Code().createLiteral(value.length));
287: throw new SkipException(fieldAccess);
288: }
289: }
290: }
291: if (Parameters.isParameterSource(ref)) {
292: // replace direct field parameter accesses
293: Object value = Parameters.getValue(template, ref
294: .getSimpleName(), Parameters
295: .getIndex(fieldAccess));
296: CtElement toReplace = fieldAccess;
297: if (fieldAccess.getParent() instanceof CtArrayAccess) {
298: toReplace = fieldAccess.getParent();
299: }
300: if (!(value instanceof TemplateParameter)) {
301: if (value instanceof Class) {
302: toReplace.replace(f.Code().createClassAccess(
303: f.Type().createReference(
304: ((Class) value).getName())));
305: } else if (value instanceof Enum) {
306: CtTypeReference<?> enumType = f.Type()
307: .createReference(value.getClass());
308: toReplace.replace(f.Code()
309: .createVariableAccess(
310: f.Field().createReference(
311: enumType, enumType,
312: ((Enum) value).name()),
313: true));
314: } else if (value instanceof List) {
315: // replace list of CtParameter for generic access to the
316: // parameters
317: List<CtParameter<?>> l = (List<CtParameter<?>>) value;
318: List<CtExpression<?>> vas = f.Code()
319: .createVariableAccesses(l);
320: CtAbstractInvocation<?> inv = (CtAbstractInvocation<?>) fieldAccess
321: .getParent();
322: int i = inv.getArguments().indexOf(fieldAccess);
323: inv.getArguments().remove(i);
324: inv.getExecutable().getParameterTypes().remove(
325: i);
326: for (CtExpression<?> va : vas) {
327: va.setParent(fieldAccess.getParent());
328: inv.getArguments().add(i, va);
329: inv.getExecutable().getParameterTypes()
330: .add(i, va.getType());
331: i++;
332: }
333: } else if ((value != null)
334: && value.getClass().isArray()) {
335: toReplace.replace(f.Code().createLiteralArray(
336: (Object[]) value));
337: } else {
338: toReplace
339: .replace(f.Code().createLiteral(value));
340: }
341: } else {
342: toReplace.replace(((TemplateParameter<?>) value)
343: .getSubstitution(targetType));
344: }
345: // do not visit if replaced
346: throw new SkipException(fieldAccess);
347: }
348: super .visitCtFieldAccess(fieldAccess);
349: }
350:
351: /**
352: * Replaces _xx_.S().
353: */
354: @Override
355: public <T> void visitCtInvocation(CtInvocation<T> invocation) {
356: if (invocation.getExecutable().isOverriding(S)) {
357: CtFieldAccess<?> fa = null;
358: if ((invocation.getTarget() instanceof CtFieldAccess)) {
359: fa = (CtFieldAccess<?>) invocation.getTarget();
360: }
361: if (((invocation.getTarget() instanceof CtArrayAccess) && (((CtArrayAccess<?, CtExpression<?>>) invocation
362: .getTarget()).getTarget() instanceof CtFieldAccess))) {
363: fa = (CtFieldAccess<?>) ((CtArrayAccess<?, CtExpression<?>>) invocation
364: .getTarget()).getTarget();
365: }
366: if ((fa != null) && (fa.getTarget() == null)) {
367: TemplateParameter<?> tparamValue = (TemplateParameter<?>) Parameters
368: .getValue(template, fa.getVariable()
369: .getSimpleName(), Parameters
370: .getIndex(fa));
371: CtCodeElement r = null;
372: if (tparamValue != null) {
373: r = tparamValue.getSubstitution(targetType);
374: // substitute in the replacement (for fixing type
375: // references
376: // and
377: // for recursive substitution)
378: r.accept(parent);
379: }
380: if ((invocation.getParent() instanceof CtReturn)
381: && (r instanceof CtBlock)) {
382: // block template parameters in returns should
383: // replace
384: // the return
385: invocation.getParent().replace(r);
386: } else {
387: invocation.replace(r);
388: }
389: }
390: // do not visit the invocation if replaced
391: throw new SkipException(invocation);
392: }
393: super .visitCtInvocation(invocation);
394: }
395:
396: @SuppressWarnings("unchecked")
397: @Override
398: public <T> void scanCtExpression(CtExpression<T> expression) {
399: for (int i = 0; i < expression.getTypeCasts().size(); i++) {
400: CtTypeReference<T> t = (CtTypeReference<T>) expression
401: .getTypeCasts().get(i);
402: if (parameterNames.contains(t.getSimpleName())) {
403: // replace type parameters
404: // TODO: this would probably not work with inner classes!!!
405: Object o = Parameters.getValue(template, t
406: .getSimpleName(), null);
407: if (o instanceof Class) {
408: t = f.Type().createReference(((Class<T>) o));
409: } else if (o instanceof CtTypeReference) {
410: t = (CtTypeReference<T>) o;
411: expression.getTypeCasts().set(i, t);
412: } else {
413: throw new RuntimeException(
414: "unsupported reference substitution");
415: }
416: }
417: }
418: if (expression instanceof CtLiteral) {
419: CtLiteral lit = (CtLiteral) expression;
420: if (lit.getValue() instanceof CtTypeReference) {
421: CtTypeReference t = (CtTypeReference) lit
422: .getValue();
423: if (parameterNames.contains(t.getSimpleName())) {
424: // replace type parameters
425: // TODO: this would probably not work with inner
426: // classes!!!
427: Object o = Parameters.getValue(template, t
428: .getSimpleName(), null);
429: if (o instanceof Class) {
430: t = f.Type()
431: .createReference(((Class<T>) o));
432: } else if (o instanceof CtTypeReference) {
433: t = (CtTypeReference<T>) o;
434: lit.setValue(t);
435: } else {
436: throw new RuntimeException(
437: "unsupported reference substitution");
438: }
439: }
440: }
441: }
442: super .scanCtExpression(expression);
443: }
444:
445: @SuppressWarnings("unchecked")
446: @Override
447: public <T> void scanCtTypedElement(CtTypedElement<T> e) {
448: if ((e.getType() != null)
449: && parameterNames.contains(e.getType()
450: .getSimpleName())) {
451: // replace type parameters
452: // TODO: this would probably not work with inner classes!!!
453: CtTypeReference<T> t;
454: Object o = Parameters.getValue(template, e.getType()
455: .getSimpleName(), null);
456: if (o instanceof Class) {
457: // TODO: CHECK THAT THIS IS STILL WORKING
458: o = f.Type().createReference(((Class<T>) o));
459: }
460: if (o instanceof CtTypeReference) {
461: if ((e.getType() instanceof CtArrayTypeReference)
462: && !(o instanceof CtArrayTypeReference)) {
463: t = (CtArrayTypeReference<T>) e.getFactory()
464: .Type().createArrayReference(
465: (CtTypeReference<?>) o,
466: ((CtArrayTypeReference<?>) e
467: .getType())
468: .getDimensionCount());
469: } else {
470: t = (CtTypeReference<T>) o;
471: }
472: e.setType(t);
473: } else {
474: throw new RuntimeException(
475: "unsupported reference substitution");
476: }
477: }
478: super .scanCtTypedElement(e);
479: }
480:
481: // fixes the references to executables in templates
482: @Override
483: public <T> void visitCtExecutableReference(
484: CtExecutableReference<T> reference) {
485: scanCtReference(reference);
486: visitCtTypeReference(reference.getDeclaringType());
487: scanCtGenericElementReference(reference);
488: }
489:
490: /**
491: * Replaces type parameters and references to the template type with
492: * references to the target type (only if the referenced element exists
493: * in the target).
494: */
495: @Override
496: public <T> void visitCtTypeReference(
497: CtTypeReference<T> reference) {
498: // if (reference.equals(templateRef) || (!reference.isPrimitif() &&
499: // f.Type().createReference(Template.class).isAssignableFrom(reference)
500: // && reference.isAssignableFrom(templateRef))) {
501: if (reference.equals(templateRef)) {
502: // replace references to the template type with references
503: // to the targetType (only if the referenced element exists
504: // in the target)
505: reference
506: .setDeclaringType(targetRef.getDeclaringType());
507: reference.setPackage(targetRef.getPackage());
508: reference.setSimpleName(targetRef.getSimpleName());
509: }
510: if (parameterNames.contains(reference.getSimpleName())) {
511: // replace type parameters
512: // TODO: this would probably not work with inner classes!!!
513: CtTypeReference<?> t;
514: Object o = Parameters.getValue(template, reference
515: .getSimpleName(), null);
516: if (o instanceof Class) {
517: t = f.Type().createReference(((Class<?>) o));
518: } else if (o instanceof CtTypeReference) {
519: t = (CtTypeReference<?>) o;
520: reference.setActualTypeArguments(t
521: .getActualTypeArguments());
522: } else {
523: throw new RuntimeException(
524: "unsupported reference substitution");
525: }
526: reference.setPackage(t.getPackage());
527: reference.setSimpleName(t.getSimpleName());
528: reference.setDeclaringType(t.getDeclaringType());
529: } else if (templateTypeRef.isAssignableFrom(reference)) {
530: // this can only be a template inheritance case (to be verified)
531: CtTypeReference<?> sc = targetRef.getSuperclass();
532: if (sc != null) {
533: reference.setDeclaringType(sc.getDeclaringType());
534: reference.setPackage(sc.getPackage());
535: reference.setSimpleName(sc.getSimpleName());
536: } else {
537: reference.setDeclaringType(null);
538: reference.setPackage(f.Package().createReference(
539: "java.lang"));
540: reference.setSimpleName("Object");
541: }
542: }
543: super .visitCtTypeReference(reference);
544: }
545:
546: @SuppressWarnings("unchecked")
547: @Override
548: public <T> void visitCtVariableAccess(
549: CtVariableAccess<T> variableAccess) {
550: String name = variableAccess.getVariable().getSimpleName();
551: for (String pname : parameterNames) {
552: if (name.contains(pname)) {
553: Object value = Parameters.getValue(template, pname,
554: null);
555: if ((value instanceof List) && name.equals(pname)) {
556: // replace list of CtParameter for generic access to the
557: // parameters
558: List<CtParameter<?>> l = (List<CtParameter<?>>) value;
559: List<CtExpression<?>> vas = f.Code()
560: .createVariableAccesses(l);
561: CtAbstractInvocation<?> inv = (CtAbstractInvocation<?>) variableAccess
562: .getParent();
563: int i = inv.getArguments().indexOf(
564: variableAccess);
565: inv.getArguments().remove(i);
566: inv.getExecutable().getParameterTypes().remove(
567: i);
568: for (CtExpression<?> va : vas) {
569: va.setParent(variableAccess.getParent());
570: inv.getArguments().add(i, va);
571: inv.getExecutable().getParameterTypes()
572: .add(i, va.getType());
573: i++;
574: }
575: // inv.getArguments().remove(variableAccess);
576: throw new SkipException(variableAccess);
577: }
578: // replace variable accesses names
579: if (value instanceof String) {
580: name = name.replace(pname, (String) value);
581: variableAccess.getVariable()
582: .setSimpleName(name);
583: }
584: }
585: }
586: CtTypeReference<T> reference = variableAccess.getType();
587: if ((parameterNames != null)
588: && (reference != null)
589: && parameterNames.contains(reference
590: .getSimpleName())) {
591: CtTypeReference<T> t;
592: Object o = Parameters.getValue(template, reference
593: .getSimpleName(), null);
594: if (o instanceof Class) {
595: t = f.Type().createReference(((Class<T>) o));
596: } else if (o instanceof CtTypeReference) {
597: t = (CtTypeReference<T>) o;
598: reference.setActualTypeArguments(t
599: .getActualTypeArguments());
600: } else {
601: throw new RuntimeException(
602: "unsupported reference substitution");
603: }
604: variableAccess.setType(t);
605: }
606: super .visitCtVariableAccess(variableAccess);
607: }
608:
609: }
610:
611: Factory f;
612:
613: InheritanceSustitutionScanner inheritanceScanner;
614:
615: CtExecutableReference<?> S;
616:
617: CtTypeReference<?> targetRef;
618:
619: CtSimpleType<?> targetType;
620:
621: Template template;
622:
623: CtTypeReference<? extends Template> templateRef;
624:
625: CtTypeReference<Template> templateTypeRef;
626:
627: CtClass<? extends Template> templateType;
628:
629: Collection<String> parameterNames;
630:
631: /**
632: * Creates a new substitution visitor.
633: *
634: * @param f
635: * the factory
636: * @param targetType
637: * the target type of the substitution
638: * @param template
639: * the template that holds the parameter values
640: */
641: public SubstitutionVisitor(Factory f, CtSimpleType<?> targetType,
642: Template template) {
643: inheritanceScanner = new InheritanceSustitutionScanner(this );
644: this .f = f;
645: this .template = template;
646: this .targetType = targetType;
647: S = f.Executable().createReference(
648: f.Type().createReference(TemplateParameter.class),
649: f.Type().createTypeParameterReference("T"), "S");
650: templateRef = f.Type().createReference(template.getClass());
651: templateType = f.Template().get(templateRef.getQualifiedName());
652: parameterNames = Parameters.getNames(templateType);
653: targetRef = f.Type().createReference(targetType);
654: // substitute target ref
655: targetRef.accept(this );
656: templateTypeRef = f.Type().createReference(Template.class);
657: }
658:
659: /**
660: * Override to scan on collection copies and avoid potential concurrent
661: * modification exceptions.
662: */
663: @Override
664: public void scan(Collection<? extends CtElement> elements) {
665: super .scan(new ArrayList<CtElement>(elements));
666: }
667:
668: @Override
669: public void scan(CtElement element) {
670: try {
671: inheritanceScanner.scan(element);
672: super .scan(element);
673: } catch (SkipException e) {
674: // System.out.println(e.getMessage());
675: } catch (UndefinedParameterException upe) {
676: removeEnclosingStatement(element);
677: }
678: }
679:
680: private void removeEnclosingStatement(CtElement e) {
681: if (!(e.getParent() instanceof CtBlock)) {
682: removeEnclosingStatement(e.getParent());
683: } else {
684: e.replace(null);
685: }
686: }
687:
688: /**
689: * Replaces parameters in reference names (even if detected as a substring).
690: */
691: @Override
692: public void scan(CtReference reference) {
693: if (reference == null) {
694: return;
695: }
696: inheritanceScanner.scan(reference);
697: if (!(reference instanceof CtTypeReference)) {
698: // replace parameters in reference names
699: String name = reference.getSimpleName();
700: for (String pname : parameterNames) {
701: if (name.contains(pname)) {
702: name = name.replace(pname, (String) Parameters
703: .getValue(template, pname, null));
704: reference.setSimpleName(name);
705: }
706: }
707: super .scan(reference);
708: } else {
709: if (!(parameterNames.contains(reference.getSimpleName())
710: && (((CtTypeReference<?>) reference)
711: .getDeclaringType() != null) && ((CtTypeReference<?>) reference)
712: .getDeclaringType().equals(templateRef))) {
713: super.scan(reference);
714: }
715: }
716: }
717: }
|