View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jexl3.internal;
18  
19  import java.util.Collections;
20  import java.util.EnumSet;
21  import java.util.Set;
22  import java.util.function.Consumer;
23  
24  import org.apache.commons.jexl3.JexlArithmetic;
25  import org.apache.commons.jexl3.JexlCache;
26  import org.apache.commons.jexl3.JexlEngine;
27  import org.apache.commons.jexl3.JexlException;
28  import org.apache.commons.jexl3.JexlOperator;
29  import org.apache.commons.jexl3.internal.introspection.MethodExecutor;
30  import org.apache.commons.jexl3.internal.introspection.MethodKey;
31  import org.apache.commons.jexl3.introspection.JexlMethod;
32  import org.apache.commons.jexl3.introspection.JexlUberspect;
33  import org.apache.commons.jexl3.parser.JexlNode;
34  
35  /**
36   * Helper class to deal with operator overloading and specifics.
37   *
38   * @since 3.0
39   */
40  public final class Operator implements JexlOperator.Uberspect {
41      private static final String METHOD_IS_EMPTY = "isEmpty";
42      private static final String METHOD_SIZE = "size";
43      private static final String METHOD_CONTAINS = "contains";
44      private static final String METHOD_STARTS_WITH = "startsWith";
45      private static final String METHOD_ENDS_WITH = "endsWith";
46  
47      /**
48       * The comparison operators.
49       * <p>Used to determine if a compare method overload might be used.</p>
50       */
51      private static final Set<JexlOperator> CMP_OPS =
52              EnumSet.of(JexlOperator.GT, JexlOperator.LT, JexlOperator.EQ, JexlOperator.GTE, JexlOperator.LTE);
53  
54      /**
55       * The postfix operators.
56       * <p>Used to determine the returned value in assignment.</p>
57       */
58      private static final Set<JexlOperator> POSTFIX_OPS =
59              EnumSet.of(JexlOperator.GET_AND_INCREMENT, JexlOperator.GET_AND_DECREMENT);
60  
61      /** The uberspect. */
62      private final JexlUberspect uberspect;
63  
64      /** The arithmetic instance being analyzed. */
65      private final JexlArithmetic arithmetic;
66  
67      /** The set of overloaded operators. */
68      private final Set<JexlOperator> overloads;
69  
70      /** The delegate if built as a 3.4 legacy. */
71      private final JexlArithmetic.Uberspect delegate;
72  
73      /** Caching state: -1 unknown, 0 false, 1 true. */
74      private volatile int caching = -1;
75  
76      /**
77       * Creates an instance.
78       * <p>Mostly used as a compatibility measure by delegating instead of extending.</p>
79       *
80       * @param theUberspect  The uberspect instance
81       * @param theArithmetic The arithmetic instance used to delegate operator overloads
82       */
83      public Operator(final JexlUberspect theUberspect, final JexlArithmetic theArithmetic) {
84          this.uberspect = theUberspect;
85          this.arithmetic = theArithmetic;
86          this.overloads = Collections.emptySet();
87          this.delegate = theUberspect.getArithmetic(theArithmetic);
88      }
89  
90      /**
91       * Creates an instance.
92       *
93       * @param theUberspect The uberspect instance
94       * @param theArithmetic The arithmetic instance
95       * @param theOverloads  The overloaded operators
96       */
97      public Operator(final JexlUberspect theUberspect,
98                      final JexlArithmetic theArithmetic,
99                      final Set<JexlOperator> theOverloads) {
100         this(theUberspect, theArithmetic, theOverloads, -1);
101     }
102 
103     /**
104      * Creates an instance.
105      *
106      * @param theUberspect The uberspect instance
107      * @param theArithmetic The arithmetic instance
108      * @param theOverloads  The overloaded operators
109      * @param theCache The caching state
110      */
111     public Operator(final JexlUberspect theUberspect,
112                     final JexlArithmetic theArithmetic,
113                     final Set<JexlOperator> theOverloads,
114                     final int theCache) {
115         this.uberspect = theUberspect;
116         this.arithmetic = theArithmetic;
117         this.overloads = theOverloads;
118         this.delegate = null;
119         this.caching = theCache;
120     }
121 
122     @Override
123     public JexlMethod getOperator(final JexlOperator operator, final Object... args) {
124         if (delegate != null) {
125             return delegate.getOperator(operator, args);
126         }
127         if (overloads.contains(operator) && args != null && args.length == operator.getArity()) {
128             return uberspectOperator(arithmetic, operator, args);
129         }
130         return null;
131     }
132 
133     @Override
134     public boolean overloads(final JexlOperator operator) {
135         return delegate != null
136             ? delegate.overloads(operator)
137             : overloads.contains(operator);
138     }
139 
140     /**
141      * @return whether caching is enabled in the engine
142      */
143     private boolean isCaching() {
144         int c = caching;
145         if (c < 0) {
146             synchronized(this) {
147                 c = caching;
148                 if (c < 0) {
149                     final JexlEngine jexl = JexlEngine.getThreadEngine();
150                     caching = c = jexl instanceof Engine && ((Engine) jexl).cache != null ? 1 : 0;
151                 }
152             }
153         }
154         return c > 0;
155     }
156 
157     /**
158      * Tidy arguments based on operator arity.
159      * <p>The interpreter may add a null to the arguments of operator expecting only one parameter.</p>
160      *
161      * @param operator The operator
162      * @param args The arguments (as seen by the interpreter)
163      * @return The tidied arguments
164      */
165     private Object[] arguments(final JexlOperator operator, final Object...args) {
166         return operator.getArity() == 1 && args.length > 1 ? new Object[]{args[0]} : args;
167     }
168 
169     /**
170      * Attempts finding a method in left and eventually narrowing right.
171      *
172      * @param methodName The method name
173      * @param right The left argument in the operator
174      * @param left The right argument in the operator
175      * @return A boolean is call was possible, null otherwise
176      * @throws Exception Thrown if invocation fails
177      */
178     private Boolean booleanDuckCall(final String methodName, final Object left, final Object right) throws Exception {
179         JexlMethod vm = uberspect.getMethod(left, methodName, right);
180         if (returnsBoolean(vm)) {
181             return (Boolean) vm.invoke(left, right);
182         }
183         final Object[] argv = { right };
184         if (arithmetic.narrowArguments(argv)) {
185             vm = uberspect.getMethod(left, methodName, argv);
186             if (returnsBoolean(vm)) {
187                 return (Boolean) vm.invoke(left, argv);
188             }
189         }
190         return null;
191     }
192 
193     /**
194      * Throw a NPE if operator is strict and one of the arguments is null.
195      *
196      * @param arithmetic The JEXL arithmetic instance
197      * @param operator The operator to check
198      * @param args The operands
199      * @throws JexlArithmetic.NullOperand if operator is strict and an operand is null
200      */
201      private void controlNullOperands(final JexlArithmetic arithmetic, final JexlOperator operator, final Object...args) {
202         for (final Object arg : args) {
203             // only check operator if necessary
204             if (arg == null) {
205                 // check operator only once if it is not strict
206                 if (arithmetic.isStrict(operator)) {
207                     throw new JexlArithmetic.NullOperand();
208                 }
209                 break;
210             }
211         }
212     }
213 
214     /**
215      * Triggered when an operator fails.
216      *
217      * @param ref     The node where the error originated from
218      * @param operator The operator symbol
219      * @param cause    The cause of error (if any)
220      * @param alt      what to return if not strict
221      * @param <T>      the return type
222      * @return throws JexlException if strict and not silent, null otherwise
223      */
224     private <T> T operatorError(final JexlCache.Reference ref, final JexlOperator operator, final Throwable cause, final T alt) {
225         final JexlNode node = ref instanceof JexlNode ? (JexlNode) ref : null;
226         final Engine engine = (Engine) JexlEngine.getThreadEngine();
227         if (engine == null || engine.isStrict()) {
228             throw new JexlException.Operator(node, operator.getOperatorSymbol(), cause);
229         }
230         if (engine.logger.isDebugEnabled()) {
231             engine.logger.debug(JexlException.operatorError(node, operator.getOperatorSymbol()), cause);
232         }
233         return alt;
234     }
235 
236     /**
237      * Seeks an implementation of an operator method in an arithmetic instance.
238      * <p>Method must <em><>not/em belong to JexlArithmetic</p>
239      *
240      * @param arithmetic The arithmetic instance
241      * @param operator The operator
242      * @param args The arguments
243      * @return A JexlMethod instance or null
244      */
245     private JexlMethod uberspectOperator(final JexlArithmetic arithmetic,
246                                        final JexlOperator operator,
247                                        final Object... args) {
248         final JexlMethod me = uberspect.getMethod(arithmetic, operator.getMethodName(), args);
249         if (!(me instanceof MethodExecutor) ||
250             !JexlArithmetic.class.equals(((MethodExecutor) me).getMethod().getDeclaringClass())) {
251             return me;
252         }
253         return null;
254     }
255 
256     /**
257      * Checks whether a method returns a boolean or a Boolean.
258      *
259      * @param vm The JexlMethod (can be null)
260      * @return true of false
261      */
262     private boolean returnsBoolean(final JexlMethod vm) {
263         if (vm != null) {
264             final Class<?> rc = vm.getReturnType();
265             return Boolean.TYPE.equals(rc) || Boolean.class.equals(rc);
266         }
267         return false;
268     }
269 
270     /**
271      * Checks whether a method returns an int or an Integer.
272      *
273      * @param vm The JexlMethod (can be null)
274      * @return true of false
275      */
276     private boolean returnsInteger(final JexlMethod vm) {
277         if (vm != null) {
278             final Class<?> rc = vm.getReturnType();
279             return Integer.TYPE.equals(rc) || Integer.class.equals(rc);
280         }
281         return false;
282     }
283 
284     @Override
285     public Object empty(final JexlCache.Reference node, final Object object) {
286         if (object == null) {
287             return true;
288         }
289         Object result = overloads(JexlOperator.EMPTY)
290                 ? tryOverload(node, JexlOperator.EMPTY, object)
291                 : JexlEngine.TRY_FAILED;
292         if (result == JexlEngine.TRY_FAILED) {
293             result = arithmetic.isEmpty(object, null);
294             if (result == null) {
295                 result = false;
296                 // check if there is an isEmpty method on the object that returns a
297                 // boolean and if so, just use it
298                 final JexlMethod vm = uberspect.getMethod(object, METHOD_IS_EMPTY, InterpreterBase.EMPTY_PARAMS);
299                 if (returnsBoolean(vm)) {
300                     try {
301                         result = vm.invoke(object, InterpreterBase.EMPTY_PARAMS);
302                     } catch (final Exception any) {
303                         return operatorError(node, JexlOperator.EMPTY, any, false);
304                     }
305                 }
306             }
307         }
308         return result;
309     }
310 
311     @Override
312     public Object size(final JexlCache.Reference node, final Object object) {
313         if (object == null) {
314             return 0;
315         }
316         Object result = overloads(JexlOperator.SIZE)
317                 ? tryOverload(node, JexlOperator.SIZE, object)
318                 : JexlEngine.TRY_FAILED;
319         if (result == JexlEngine.TRY_FAILED) {
320             result = arithmetic.size(object, null);
321             if (result == null) {
322                 // check if there is a size method on the object that returns an
323                 // integer and if so, just use it
324                 final JexlMethod vm = uberspect.getMethod(object, METHOD_SIZE, InterpreterBase.EMPTY_PARAMS);
325                 if (returnsInteger(vm)) {
326                     try {
327                         result = vm.invoke(object, InterpreterBase.EMPTY_PARAMS);
328                     } catch (final Exception any) {
329                         return operatorError(node, JexlOperator.SIZE, any, 0);
330                     }
331                 }
332             }
333         }
334         return result instanceof Number ? ((Number) result).intValue() : 0;
335     }
336 
337     @Override
338     public boolean contains(final JexlCache.Reference node, final JexlOperator operator, final Object left, final Object right) {
339         final boolean contained;
340         try {
341             // try operator overload
342             final Object result = overloads(JexlOperator.CONTAINS)
343                     ? tryOverload(node, JexlOperator.CONTAINS, left, right)
344                     : null;
345             if (result instanceof Boolean) {
346                 contained = (Boolean) result;
347             } else {
348                 // use arithmetic / pattern matching ?
349                 final Boolean matched = arithmetic.contains(left, right);
350                 if (matched != null) {
351                     contained = matched;
352                 } else {
353                     // try a left.contains(right) method
354                     final Boolean duck = booleanDuckCall(METHOD_CONTAINS, left, right);
355                     if (duck != null) {
356                         contained = duck;
357                     } else {
358                         // defaults to equal
359                         contained = arithmetic.equals(left, right);
360                     }
361                 }
362             }
363             // not-contains is !contains
364             return JexlOperator.CONTAINS == operator == contained;
365         } catch (final Exception any) {
366             if (Thread.currentThread().isInterrupted()) {
367                 throw new JexlException.Cancel(node instanceof JexlNode ? (JexlNode) node : null);
368             }
369             return operatorError(node, operator, any, false);
370         }
371     }
372 
373     @Override
374     public boolean startsWith(final JexlCache.Reference node, final JexlOperator operator, final Object left, final Object right) {
375         final boolean starts;
376         try {
377             // try operator overload
378             final Object result = overloads(JexlOperator.STARTSWITH)
379                     ? tryOverload(node, JexlOperator.STARTSWITH, left, right)
380                     : null;
381             if (result instanceof Boolean) {
382                 starts = (Boolean) result;
383             } else {
384                 // use arithmetic / pattern matching ?
385                 final Boolean matched = arithmetic.startsWith(left, right);
386                 if (matched != null) {
387                     starts = matched;
388                 } else {
389                     // try a left.startsWith(right) method
390                     final Boolean duck = booleanDuckCall(METHOD_STARTS_WITH, left, right);
391                     if (duck != null) {
392                         starts = duck;
393                     } else {
394                         // defaults to equal
395                         starts = arithmetic.equals(left, right);
396                     }
397                 }
398             }
399             // not-startswith is !starts-with
400             return JexlOperator.STARTSWITH == operator == starts;
401         } catch (final Exception any) {
402             return operatorError(node, operator, any, false);
403         }
404     }
405 
406     @Override
407     public boolean endsWith(final JexlCache.Reference node, final JexlOperator operator, final Object left, final Object right) {
408         try {
409             final boolean ends;
410             // try operator overload
411             final Object result = overloads(JexlOperator.ENDSWITH)
412                 ? tryOverload(node, JexlOperator.ENDSWITH, left, right)
413                 : null;
414             if (result instanceof Boolean) {
415                 ends = (Boolean) result;
416             } else {
417                 // use arithmetic / pattern matching ?
418                 final Boolean matched = arithmetic.endsWith(left, right);
419                 if (matched != null) {
420                     ends = matched;
421                 } else {
422                     // try a left.endsWith(right) method
423                     final Boolean duck = booleanDuckCall(METHOD_ENDS_WITH, left, right);
424                     if (duck != null) {
425                         ends = duck;
426                     } else {
427                         // defaults to equal
428                         ends = arithmetic.equals(left, right);
429                     }
430                 }
431             }
432             // not-endswith is !ends-with
433             return JexlOperator.ENDSWITH == operator == ends;
434         } catch (final Exception any) {
435             return operatorError(node, operator, any, false);
436         }
437     }
438 
439     @Override
440     public Object tryAssignOverload(final JexlCache.Reference node,
441                              final JexlOperator operator,
442                              final Consumer<Object> assignFun,
443                              final Object... args) {
444         if (args.length < operator.getArity()) {
445             return JexlEngine.TRY_FAILED;
446         }
447         Object result = JexlEngine.TRY_FAILED;
448         try {
449             // if the operator is strict, the left-hand side can not be null
450             controlNullOperands(arithmetic, operator, args[0]);
451             // attempt assignment operator overload
452             if (overloads(operator)) {
453                 result = tryOverload(node, operator, arguments(operator, args));
454                 if (result != JexlEngine.TRY_FAILED) {
455                     return result;
456                 }
457             }
458             // let's attempt base operator overload
459             final JexlOperator base = operator.getBaseOperator();
460             if (base != null && overloads(base)) {
461                 result = tryOverload(node, base, arguments(base, args));
462             }
463             // no overload or overload failed, use base operator
464             if (result == JexlEngine.TRY_FAILED) {
465                 result = performBaseOperation(operator, args);
466             }
467             // on success, assign value
468             if (result != JexlEngine.TRY_FAILED) {
469                 assignFun.accept(result);
470                 // postfix implies return initial argument value
471                 if (POSTFIX_OPS.contains(operator)) {
472                     result = args[0];
473                 }
474             }
475             return result;
476         } catch (final Exception any) {
477             return operatorError(node, operator, any, JexlEngine.TRY_FAILED);
478         }
479     }
480 
481     /**
482      * Performs the base operation of an assignment.
483      *
484      * @param operator The operator
485      * @param args The arguments
486      * @return The result
487      */
488     private Object performBaseOperation(final JexlOperator operator, final Object... args) {
489         switch (operator) {
490             case SELF_ADD: return arithmetic.add(args[0], args[1]);
491             case SELF_SUBTRACT: return arithmetic.subtract(args[0], args[1]);
492             case SELF_MULTIPLY: return arithmetic.multiply(args[0], args[1]);
493             case SELF_DIVIDE: return arithmetic.divide(args[0], args[1]);
494             case SELF_MOD: return arithmetic.mod(args[0], args[1]);
495             case SELF_AND: return arithmetic.and(args[0], args[1]);
496             case SELF_OR: return arithmetic.or(args[0], args[1]);
497             case SELF_XOR: return arithmetic.xor(args[0], args[1]);
498             case SELF_SHIFTLEFT: return arithmetic.shiftLeft(args[0], args[1]);
499             case SELF_SHIFTRIGHT: return arithmetic.shiftRight(args[0], args[1]);
500             case SELF_SHIFTRIGHTU: return arithmetic.shiftRightUnsigned(args[0], args[1]);
501             case INCREMENT_AND_GET:
502             case GET_AND_INCREMENT:
503                 return arithmetic.increment(args[0]);
504             case DECREMENT_AND_GET:
505             case GET_AND_DECREMENT:
506                 return arithmetic.decrement(args[0]);
507             default:
508                 throw new UnsupportedOperationException(operator.getOperatorSymbol());
509         }
510     }
511 
512     @Override
513     public Object tryOverload(final JexlCache.Reference node, final JexlOperator operator, final Object... args) {
514         controlNullOperands(arithmetic, operator, args);
515         try {
516             return tryEval(isCaching() ? node : null, operator, args);
517         } catch (final Exception any) {
518             // ignore return if lenient, will return try_failed
519             return operatorError(node, operator, any, JexlEngine.TRY_FAILED);
520         }
521     }
522 
523     /**
524      * Tries operator evaluation, handles method resolution caching.
525      *
526      * @param node The node
527      * @param operator The operator
528      * @param args The operator arguments
529      * @return The operator evaluation result or TRY_FAILED
530      */
531     private Object tryEval(final JexlCache.Reference node, final JexlOperator operator, final Object...args) {
532         if (node != null) {
533             final Object cached = node.getCache();
534             if (cached instanceof JexlMethod) {
535                 // we found a method on previous call; try and reuse it (*1)
536                 final JexlMethod me = (JexlMethod) cached;
537                 final Object eval = me.tryInvoke(operator.getMethodName(), arithmetic, args);
538                 if (!me.tryFailed(eval)) {
539                     return eval;
540                 }
541             } else if (cached instanceof MethodKey) {
542                 // check for a fail-fast, we tried to find an overload before but could not (*2)
543                 final MethodKey cachedKey = (MethodKey) cached;
544                 final MethodKey key = new MethodKey(operator.getMethodName(), args);
545                 if (key.equals(cachedKey)) {
546                     return JexlEngine.TRY_FAILED;
547                 }
548             }
549         }
550         // trying to find an operator overload
551         JexlMethod vm = getOperator(operator, args);
552         // no direct overload, any special case ?
553         if (vm == null) {
554             vm = getAlternateOverload(operator, args);
555         }
556         // *1: found a method, try it and cache it if successful
557         if (vm != null) {
558             final Object result = vm.tryInvoke(operator.getMethodName(), arithmetic, args);
559             if (node != null && !vm.tryFailed(result)) {
560                 node.setCache(vm);
561             }
562             return result;
563         }
564         if (node != null) {
565             // *2: could not find an overload for this operator and arguments, keep track of the fail
566             final MethodKey key = new MethodKey(operator.getMethodName(), args);
567             node.setCache(key);
568         }
569         return JexlEngine.TRY_FAILED;
570     }
571 
572     /**
573      * Special handling of overloads where another attempt at finding a method may be attempted.
574      * <p>As of 3.5.0, only the comparison operators attempting to use compare() are handled.</p>
575      *
576      * @param operator The operator
577      * @param args The arguments
578      * @return An instance or null
579      */
580     private JexlMethod getAlternateOverload(final JexlOperator operator, final Object... args) {
581         // comparison operators may use the compare overload in derived arithmetic
582         if (CMP_OPS.contains(operator) && args.length == 2) {
583             JexlMethod cmp = getOperator(JexlOperator.COMPARE, args);
584             if (cmp != null) {
585                 return new Operator.CompareMethod(operator, cmp);
586             }
587             cmp = getOperator(JexlOperator.COMPARE, args[1], args[0]);
588             if (cmp != null) {
589                 return new Operator.AntiCompareMethod(operator, cmp);
590             }
591         }
592         return null;
593     }
594 
595     /**
596      * Delegates a comparison operator to a compare method.
597      * The expected signature of the derived JexlArithmetic method is:
598      * int compare(L left, R right);
599      */
600     private static class CompareMethod implements JexlMethod {
601         protected final JexlOperator operator;
602         protected final JexlMethod compare;
603 
604         CompareMethod(final JexlOperator op, final JexlMethod m) {
605             operator = op;
606             compare = m;
607         }
608 
609         @Override
610         public Class<?> getReturnType() {
611             return Boolean.TYPE;
612         }
613 
614         @Override
615         public Object invoke(final Object arithmetic, final Object... params) throws Exception {
616             return operate((int) compare.invoke(arithmetic, params));
617         }
618 
619         @Override
620         public boolean isCacheable() {
621             return true;
622         }
623 
624         @Override
625         public boolean tryFailed(final Object rval) {
626             return rval == JexlEngine.TRY_FAILED;
627         }
628 
629         @Override
630         public Object tryInvoke(final String name, final Object arithmetic, final Object... params) throws JexlException.TryFailed {
631             final Object cmp = compare.tryInvoke(JexlOperator.COMPARE.getMethodName(), arithmetic, params);
632             return cmp instanceof Integer? operate((int) cmp) : JexlEngine.TRY_FAILED;
633         }
634 
635         /**
636          * From compare() int result to operator boolean result.
637          *
638          * @param cmp The compare result
639          * @return The operator applied
640          */
641         protected boolean operate(final int cmp) {
642             switch(operator) {
643                 case EQ: return cmp == 0;
644                 case LT: return cmp < 0;
645                 case LTE: return cmp <= 0;
646                 case GT: return cmp > 0;
647                 case GTE: return cmp >= 0;
648                 default:
649                     throw new ArithmeticException("unexpected operator " + operator);
650             }
651         }
652     }
653 
654     /**
655      * The reverse compare swaps left and right arguments and exploits the symmetry
656      * of compare as in: compare(a, b) &lt=&gt -compare(b, a)
657      */
658     private static class AntiCompareMethod extends CompareMethod {
659         AntiCompareMethod(final JexlOperator op, final JexlMethod m) {
660             super(op, m);
661         }
662 
663         @Override
664         public Object invoke(final Object arithmetic, final Object... params) throws Exception {
665             return operate(-(int) compare.invoke(arithmetic, params[1], params[0]));
666         }
667 
668         @Override
669         public Object tryInvoke(final String name, final Object arithmetic, final Object... params) throws JexlException.TryFailed {
670             final Object cmp = compare.tryInvoke(JexlOperator.COMPARE.getMethodName(), arithmetic, params[1], params[0]);
671             return cmp instanceof Integer? operate(-Integer.signum((Integer) cmp)) : JexlEngine.TRY_FAILED;
672         }
673     }
674 }