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    *      http://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.logging.log4j.core.layout;
18  
19  import java.nio.charset.Charset;
20  import java.util.Arrays;
21  import java.util.HashMap;
22  import java.util.List;
23  import java.util.Map;
24  
25  import org.apache.logging.log4j.core.Layout;
26  import org.apache.logging.log4j.core.LogEvent;
27  import org.apache.logging.log4j.core.config.Configuration;
28  import org.apache.logging.log4j.core.config.DefaultConfiguration;
29  import org.apache.logging.log4j.core.config.Node;
30  import org.apache.logging.log4j.core.config.plugins.Plugin;
31  import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
32  import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
33  import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
34  import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
35  import org.apache.logging.log4j.core.config.plugins.PluginElement;
36  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
37  import org.apache.logging.log4j.core.impl.LocationAware;
38  import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
39  import org.apache.logging.log4j.core.pattern.PatternFormatter;
40  import org.apache.logging.log4j.core.pattern.PatternParser;
41  import org.apache.logging.log4j.core.pattern.RegexReplacement;
42  import org.apache.logging.log4j.util.PropertiesUtil;
43  import org.apache.logging.log4j.util.Strings;
44  
45  /**
46   * A flexible layout configurable with pattern string.
47   * <p>
48   * The goal of this class is to {@link org.apache.logging.log4j.core.Layout#toByteArray format} a {@link LogEvent} and
49   * return the results. The format of the result depends on the <em>conversion pattern</em>.
50   * </p>
51   * <p>
52   * The conversion pattern is closely related to the conversion pattern of the printf function in C. A conversion pattern
53   * is composed of literal text and format control expressions called <em>conversion specifiers</em>.
54   * </p>
55   * <p>
56   * See the Log4j Manual for details on the supported pattern converters.
57   * </p>
58   */
59  @Plugin(name = "PatternLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
60  public final class PatternLayout extends AbstractStringLayout {
61  
62      /**
63       * Default pattern string for log output. Currently set to the string <b>"%m%n"</b> which just prints the
64       * application supplied message.
65       */
66      public static final String DEFAULT_CONVERSION_PATTERN = "%m%n";
67  
68      /**
69       * A conversion pattern equivalent to the TTCCLayout. Current value is <b>%r [%t] %p %c %notEmpty{%x }- %m%n</b>.
70       */
71      public static final String TTCC_CONVERSION_PATTERN = "%r [%t] %p %c %notEmpty{%x }- %m%n";
72  
73      /**
74       * A simple pattern. Current value is <b>%d [%t] %p %c - %m%n</b>.
75       */
76      public static final String SIMPLE_CONVERSION_PATTERN = "%d [%t] %p %c - %m%n";
77  
78      /** Key to identify pattern converters. */
79      public static final String KEY = "Converter";
80  
81      /**
82       * Conversion pattern.
83       */
84      private final String conversionPattern;
85      private final PatternSelector patternSelector;
86      private final Serializer eventSerializer;
87  
88      /**
89       * Constructs a PatternLayout using the supplied conversion pattern.
90       *
91       * @param config The Configuration.
92       * @param replace The regular expression to match.
93       * @param eventPattern conversion pattern.
94       * @param patternSelector The PatternSelector.
95       * @param charset The character set.
96       * @param alwaysWriteExceptions Whether or not exceptions should always be handled in this pattern (if {@code true},
97       *                         exceptions will be written even if the pattern does not specify so).
98       * @param disableAnsi
99       *            If {@code "true"}, do not output ANSI escape codes
100      * @param noConsoleNoAnsi
101      *            If {@code "true"} (default) and {@link System#console()} is null, do not output ANSI escape codes
102      * @param headerPattern header conversion pattern.
103      * @param footerPattern footer conversion pattern.
104      */
105     private PatternLayout(final Configuration config, final RegexReplacement replace, final String eventPattern,
106             final PatternSelector patternSelector, final Charset charset, final boolean alwaysWriteExceptions,
107             final boolean disableAnsi, final boolean noConsoleNoAnsi, final String headerPattern,
108             final String footerPattern) {
109         super(config, charset,
110                 newSerializerBuilder()
111                         .setConfiguration(config)
112                         .setReplace(replace)
113                         .setPatternSelector(patternSelector)
114                         .setAlwaysWriteExceptions(alwaysWriteExceptions)
115                         .setDisableAnsi(disableAnsi)
116                         .setNoConsoleNoAnsi(noConsoleNoAnsi)
117                         .setPattern(headerPattern)
118                         .build(),
119                 newSerializerBuilder()
120                         .setConfiguration(config)
121                         .setReplace(replace)
122                         .setPatternSelector(patternSelector)
123                         .setAlwaysWriteExceptions(alwaysWriteExceptions)
124                         .setDisableAnsi(disableAnsi)
125                         .setNoConsoleNoAnsi(noConsoleNoAnsi)
126                         .setPattern(footerPattern)
127                         .build());
128         this.conversionPattern = eventPattern;
129         this.patternSelector = patternSelector;
130         this.eventSerializer = newSerializerBuilder()
131                 .setConfiguration(config)
132                 .setReplace(replace)
133                 .setPatternSelector(patternSelector)
134                 .setAlwaysWriteExceptions(alwaysWriteExceptions)
135                 .setDisableAnsi(disableAnsi)
136                 .setNoConsoleNoAnsi(noConsoleNoAnsi)
137                 .setPattern(eventPattern)
138                 .setDefaultPattern(DEFAULT_CONVERSION_PATTERN)
139                 .build();
140     }
141 
142     public static SerializerBuilder newSerializerBuilder() {
143         return new SerializerBuilder();
144     }
145 
146     @Override
147     public boolean requiresLocation() {
148         return eventSerializer instanceof LocationAware && ((LocationAware) eventSerializer).requiresLocation();
149     }
150 
151 
152     /**
153      * Deprecated, use {@link #newSerializerBuilder()} instead.
154      *
155      * @param configuration
156      * @param replace
157      * @param pattern
158      * @param defaultPattern
159      * @param patternSelector
160      * @param alwaysWriteExceptions
161      * @param noConsoleNoAnsi
162      * @return a new Serializer.
163      * @deprecated Use {@link #newSerializerBuilder()} instead.
164      */
165     @Deprecated
166     public static Serializer createSerializer(final Configuration configuration, final RegexReplacement replace,
167             final String pattern, final String defaultPattern, final PatternSelector patternSelector,
168             final boolean alwaysWriteExceptions, final boolean noConsoleNoAnsi) {
169         final SerializerBuilder builder = newSerializerBuilder();
170         builder.setAlwaysWriteExceptions(alwaysWriteExceptions);
171         builder.setConfiguration(configuration);
172         builder.setDefaultPattern(defaultPattern);
173         builder.setNoConsoleNoAnsi(noConsoleNoAnsi);
174         builder.setPattern(pattern);
175         builder.setPatternSelector(patternSelector);
176         builder.setReplace(replace);
177         return builder.build();
178     }
179 
180     /**
181      * Gets the conversion pattern.
182      *
183      * @return the conversion pattern.
184      */
185     public String getConversionPattern() {
186         return conversionPattern;
187     }
188 
189     /**
190      * Gets this PatternLayout's content format. Specified by:
191      * <ul>
192      * <li>Key: "structured" Value: "false"</li>
193      * <li>Key: "formatType" Value: "conversion" (format uses the keywords supported by OptionConverter)</li>
194      * <li>Key: "format" Value: provided "conversionPattern" param</li>
195      * </ul>
196      *
197      * @return Map of content format keys supporting PatternLayout
198      */
199     @Override
200     public Map<String, String> getContentFormat() {
201         final Map<String, String> result = new HashMap<>();
202         result.put("structured", "false");
203         result.put("formatType", "conversion");
204         result.put("format", conversionPattern);
205         return result;
206     }
207 
208     /**
209      * Formats a logging event to a writer.
210      *
211      * @param event logging event to be formatted.
212      * @return The event formatted as a String.
213      */
214     @Override
215     public String toSerializable(final LogEvent event) {
216         return eventSerializer.toSerializable(event);
217     }
218 
219     public void serialize(final LogEvent event, StringBuilder stringBuilder) {
220         eventSerializer.toSerializable(event, stringBuilder);
221     }
222 
223     @Override
224     public void encode(final LogEvent event, final ByteBufferDestination destination) {
225         if (!(eventSerializer instanceof Serializer2)) {
226             super.encode(event, destination);
227             return;
228         }
229         final StringBuilder text = toText((Serializer2) eventSerializer, event, getStringBuilder());
230         final Encoder<StringBuilder> encoder = getStringBuilderEncoder();
231         encoder.encode(text, destination);
232         trimToMaxSize(text);
233     }
234 
235     /**
236      * Creates a text representation of the specified log event
237      * and writes it into the specified StringBuilder.
238      * <p>
239      * Implementations are free to return a new StringBuilder if they can
240      * detect in advance that the specified StringBuilder is too small.
241      */
242     private StringBuilder toText(final Serializer2 serializer, final LogEvent event,
243             final StringBuilder destination) {
244         return serializer.toSerializable(event, destination);
245     }
246 
247     /**
248      * Creates a PatternParser.
249      * @param config The Configuration.
250      * @return The PatternParser.
251      */
252     public static PatternParser createPatternParser(final Configuration config) {
253         if (config == null) {
254             return new PatternParser(config, KEY, LogEventPatternConverter.class);
255         }
256         PatternParser parser = config.getComponent(KEY);
257         if (parser == null) {
258             parser = new PatternParser(config, KEY, LogEventPatternConverter.class);
259             config.addComponent(KEY, parser);
260             parser = config.getComponent(KEY);
261         }
262         return parser;
263     }
264 
265     @Override
266     public String toString() {
267         return patternSelector == null ? conversionPattern : patternSelector.toString();
268     }
269 
270     /**
271      * Creates a pattern layout.
272      *
273      * @param pattern
274      *        The pattern. If not specified, defaults to DEFAULT_CONVERSION_PATTERN.
275      * @param patternSelector
276      *        Allows different patterns to be used based on some selection criteria.
277      * @param config
278      *        The Configuration. Some Converters require access to the Interpolator.
279      * @param replace
280      *        A Regex replacement String.
281      * @param charset
282      *        The character set. The platform default is used if not specified.
283      * @param alwaysWriteExceptions
284      *        If {@code "true"} (default) exceptions are always written even if the pattern contains no exception tokens.
285      * @param noConsoleNoAnsi
286      *        If {@code "true"} (default is false) and {@link System#console()} is null, do not output ANSI escape codes
287      * @param headerPattern
288      *        The footer to place at the top of the document, once.
289      * @param footerPattern
290      *        The footer to place at the bottom of the document, once.
291      * @return The PatternLayout.
292      * @deprecated Use {@link #newBuilder()} instead. This will be private in a future version.
293      */
294     @PluginFactory
295     @Deprecated
296     public static PatternLayout createLayout(
297             @PluginAttribute(value = "pattern", defaultString = DEFAULT_CONVERSION_PATTERN) final String pattern,
298             @PluginElement("PatternSelector") final PatternSelector patternSelector,
299             @PluginConfiguration final Configuration config,
300             @PluginElement("Replace") final RegexReplacement replace,
301             // LOG4J2-783 use platform default by default, so do not specify defaultString for charset
302             @PluginAttribute(value = "charset") final Charset charset,
303             @PluginAttribute(value = "alwaysWriteExceptions", defaultBoolean = true) final boolean alwaysWriteExceptions,
304             @PluginAttribute(value = "noConsoleNoAnsi") final boolean noConsoleNoAnsi,
305             @PluginAttribute("header") final String headerPattern,
306             @PluginAttribute("footer") final String footerPattern) {
307         return newBuilder()
308             .withPattern(pattern)
309             .withPatternSelector(patternSelector)
310             .withConfiguration(config)
311             .withRegexReplacement(replace)
312             .withCharset(charset)
313             .withAlwaysWriteExceptions(alwaysWriteExceptions)
314             .withNoConsoleNoAnsi(noConsoleNoAnsi)
315             .withHeader(headerPattern)
316             .withFooter(footerPattern)
317             .build();
318     }
319 
320     private static class PatternSerializer implements Serializer, Serializer2, LocationAware {
321 
322         private final PatternFormatter[] formatters;
323         private final RegexReplacement replace;
324 
325         private PatternSerializer(final PatternFormatter[] formatters, final RegexReplacement replace) {
326             super();
327             this.formatters = formatters;
328             this.replace = replace;
329         }
330 
331         @Override
332         public String toSerializable(final LogEvent event) {
333             final StringBuilder sb = getStringBuilder();
334             try {
335                 return toSerializable(event, sb).toString();
336             } finally {
337                 trimToMaxSize(sb);
338             }
339         }
340 
341         @Override
342         public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {
343             final int len = formatters.length;
344             for (int i = 0; i < len; i++) {
345                 formatters[i].format(event, buffer);
346             }
347             if (replace != null) { // creates temporary objects
348                 String str = buffer.toString();
349                 str = replace.format(str);
350                 buffer.setLength(0);
351                 buffer.append(str);
352             }
353             return buffer;
354         }
355 
356         @Override
357         public boolean requiresLocation() {
358             for (PatternFormatter formatter : formatters) {
359                 if (formatter.requiresLocation()) {
360                     return true;
361                 }
362             }
363             return false;
364         }
365 
366         @Override
367         public String toString() {
368             final StringBuilder builder = new StringBuilder();
369             builder.append(super.toString());
370             builder.append("[formatters=");
371             builder.append(Arrays.toString(formatters));
372             builder.append(", replace=");
373             builder.append(replace);
374             builder.append("]");
375             return builder.toString();
376         }
377     }
378 
379     public static class SerializerBuilder implements org.apache.logging.log4j.core.util.Builder<Serializer> {
380 
381         private Configuration configuration;
382         private RegexReplacement replace;
383         private String pattern;
384         private String defaultPattern;
385         private PatternSelector patternSelector;
386         private boolean alwaysWriteExceptions;
387         private boolean disableAnsi;
388         private boolean noConsoleNoAnsi;
389 
390         @Override
391         public Serializer build() {
392             if (Strings.isEmpty(pattern) && Strings.isEmpty(defaultPattern)) {
393                 return null;
394             }
395             if (patternSelector == null) {
396                 try {
397                     final PatternParser parser = createPatternParser(configuration);
398                     final List<PatternFormatter> list = parser.parse(pattern == null ? defaultPattern : pattern,
399                             alwaysWriteExceptions, disableAnsi, noConsoleNoAnsi);
400                     final PatternFormatter[] formatters = list.toArray(new PatternFormatter[0]);
401                     return new PatternSerializer(formatters, replace);
402                 } catch (final RuntimeException ex) {
403                     throw new IllegalArgumentException("Cannot parse pattern '" + pattern + "'", ex);
404                 }
405             }
406             return new PatternSelectorSerializer(patternSelector, replace);
407         }
408 
409         public SerializerBuilder setConfiguration(final Configuration configuration) {
410             this.configuration = configuration;
411             return this;
412         }
413 
414         public SerializerBuilder setReplace(final RegexReplacement replace) {
415             this.replace = replace;
416             return this;
417         }
418 
419         public SerializerBuilder setPattern(final String pattern) {
420             this.pattern = pattern;
421             return this;
422         }
423 
424         public SerializerBuilder setDefaultPattern(final String defaultPattern) {
425             this.defaultPattern = defaultPattern;
426             return this;
427         }
428 
429         public SerializerBuilder setPatternSelector(final PatternSelector patternSelector) {
430             this.patternSelector = patternSelector;
431             return this;
432         }
433 
434         public SerializerBuilder setAlwaysWriteExceptions(final boolean alwaysWriteExceptions) {
435             this.alwaysWriteExceptions = alwaysWriteExceptions;
436             return this;
437         }
438 
439         public SerializerBuilder setDisableAnsi(final boolean disableAnsi) {
440             this.disableAnsi = disableAnsi;
441             return this;
442         }
443 
444         public SerializerBuilder setNoConsoleNoAnsi(final boolean noConsoleNoAnsi) {
445             this.noConsoleNoAnsi = noConsoleNoAnsi;
446             return this;
447         }
448 
449     }
450 
451     private static class PatternSelectorSerializer implements Serializer, Serializer2, LocationAware {
452 
453         private final PatternSelector patternSelector;
454         private final RegexReplacement replace;
455 
456         private PatternSelectorSerializer(final PatternSelector patternSelector, final RegexReplacement replace) {
457             super();
458             this.patternSelector = patternSelector;
459             this.replace = replace;
460         }
461 
462         @Override
463         public String toSerializable(final LogEvent event) {
464             final StringBuilder sb = getStringBuilder();
465             try {
466                 return toSerializable(event, sb).toString();
467             } finally {
468                 trimToMaxSize(sb);
469             }
470         }
471 
472         @Override
473         public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {
474             final PatternFormatter[] formatters = patternSelector.getFormatters(event);
475             final int len = formatters.length;
476             for (int i = 0; i < len; i++) {
477                 formatters[i].format(event, buffer);
478             }
479             if (replace != null) { // creates temporary objects
480                 String str = buffer.toString();
481                 str = replace.format(str);
482                 buffer.setLength(0);
483                 buffer.append(str);
484             }
485             return buffer;
486         }
487 
488         @Override
489         public boolean requiresLocation() {
490             return patternSelector instanceof LocationAware && ((LocationAware) patternSelector).requiresLocation();
491         }
492 
493         @Override
494         public String toString() {
495             final StringBuilder builder = new StringBuilder();
496             builder.append(super.toString());
497             builder.append("[patternSelector=");
498             builder.append(patternSelector);
499             builder.append(", replace=");
500             builder.append(replace);
501             builder.append("]");
502             return builder.toString();
503         }
504     }
505 
506     /**
507      * Creates a PatternLayout using the default options. These options include using UTF-8, the default conversion
508      * pattern, exceptions being written, and with ANSI escape codes.
509      *
510      * @return the PatternLayout.
511      * @see #DEFAULT_CONVERSION_PATTERN Default conversion pattern
512      */
513     public static PatternLayout createDefaultLayout() {
514         return newBuilder().build();
515     }
516 
517     /**
518      * Creates a PatternLayout using the default options and the given configuration. These options include using UTF-8,
519      * the default conversion pattern, exceptions being written, and with ANSI escape codes.
520      *
521      * @param configuration The Configuration.
522      *
523      * @return the PatternLayout.
524      * @see #DEFAULT_CONVERSION_PATTERN Default conversion pattern
525      */
526     public static PatternLayout createDefaultLayout(final Configuration configuration) {
527         return newBuilder().withConfiguration(configuration).build();
528     }
529 
530     /**
531      * Creates a builder for a custom PatternLayout.
532      *
533      * @return a PatternLayout builder.
534      */
535     @PluginBuilderFactory
536     public static Builder newBuilder() {
537         return new Builder();
538     }
539 
540     /**
541      * Custom PatternLayout builder. Use the {@link PatternLayout#newBuilder() builder factory method} to create this.
542      */
543     public static class Builder implements org.apache.logging.log4j.core.util.Builder<PatternLayout> {
544 
545         @PluginBuilderAttribute
546         private String pattern = PatternLayout.DEFAULT_CONVERSION_PATTERN;
547 
548         @PluginElement("PatternSelector")
549         private PatternSelector patternSelector;
550 
551         @PluginConfiguration
552         private Configuration configuration;
553 
554         @PluginElement("Replace")
555         private RegexReplacement regexReplacement;
556 
557         // LOG4J2-783 use platform default by default
558         @PluginBuilderAttribute
559         private Charset charset = Charset.defaultCharset();
560 
561         @PluginBuilderAttribute
562         private boolean alwaysWriteExceptions = true;
563 
564         @PluginBuilderAttribute
565         private boolean disableAnsi = !useAnsiEscapeCodes();
566 
567         @PluginBuilderAttribute
568         private boolean noConsoleNoAnsi;
569 
570         @PluginBuilderAttribute
571         private String header;
572 
573         @PluginBuilderAttribute
574         private String footer;
575 
576         private Builder() {
577         }
578 
579         private boolean useAnsiEscapeCodes() {
580             final PropertiesUtil propertiesUtil = PropertiesUtil.getProperties();
581             final boolean isPlatformSupportsAnsi = !propertiesUtil.isOsWindows();
582             final boolean isJansiRequested = !propertiesUtil.getBooleanProperty("log4j.skipJansi", true);
583             return isPlatformSupportsAnsi || isJansiRequested;
584         }
585 
586         /**
587          * @param pattern
588          *        The pattern. If not specified, defaults to DEFAULT_CONVERSION_PATTERN.
589          */
590         public Builder withPattern(final String pattern) {
591             this.pattern = pattern;
592             return this;
593         }
594 
595         /**
596          * @param patternSelector
597          *        Allows different patterns to be used based on some selection criteria.
598          */
599         public Builder withPatternSelector(final PatternSelector patternSelector) {
600             this.patternSelector = patternSelector;
601             return this;
602         }
603 
604         /**
605          * @param configuration
606          *        The Configuration. Some Converters require access to the Interpolator.
607          */
608         public Builder withConfiguration(final Configuration configuration) {
609             this.configuration = configuration;
610             return this;
611         }
612 
613         /**
614          * @param regexReplacement
615          *        A Regex replacement
616          */
617         public Builder withRegexReplacement(final RegexReplacement regexReplacement) {
618             this.regexReplacement = regexReplacement;
619             return this;
620         }
621 
622         /**
623          * @param charset
624          *        The character set. The platform default is used if not specified.
625          */
626         public Builder withCharset(final Charset charset) {
627             // LOG4J2-783 if null, use platform default by default
628             if (charset != null) {
629                 this.charset = charset;
630             }
631             return this;
632         }
633 
634         /**
635          * @param alwaysWriteExceptions
636          *        If {@code "true"} (default) exceptions are always written even if the pattern contains no exception tokens.
637          */
638         public Builder withAlwaysWriteExceptions(final boolean alwaysWriteExceptions) {
639             this.alwaysWriteExceptions = alwaysWriteExceptions;
640             return this;
641         }
642 
643         /**
644          * @param disableAnsi
645          *        If {@code "true"} (default is value of system property `log4j.skipJansi`, or `true` if undefined),
646          *        do not output ANSI escape codes
647          */
648         public Builder withDisableAnsi(final boolean disableAnsi) {
649             this.disableAnsi = disableAnsi;
650             return this;
651         }
652 
653         /**
654          * @param noConsoleNoAnsi
655          *        If {@code "true"} (default is false) and {@link System#console()} is null, do not output ANSI escape codes
656          */
657         public Builder withNoConsoleNoAnsi(final boolean noConsoleNoAnsi) {
658             this.noConsoleNoAnsi = noConsoleNoAnsi;
659             return this;
660         }
661 
662         /**
663          * @param header
664          *        The footer to place at the top of the document, once.
665          */
666         public Builder withHeader(final String header) {
667             this.header = header;
668             return this;
669         }
670 
671         /**
672          * @param footer
673          *        The footer to place at the bottom of the document, once.
674          */
675         public Builder withFooter(final String footer) {
676             this.footer = footer;
677             return this;
678         }
679 
680         @Override
681         public PatternLayout build() {
682             // fall back to DefaultConfiguration
683             if (configuration == null) {
684                 configuration = new DefaultConfiguration();
685             }
686             return new PatternLayout(configuration, regexReplacement, pattern, patternSelector, charset,
687                 alwaysWriteExceptions, disableAnsi, noConsoleNoAnsi, header, footer);
688         }
689     }
690 
691     public Serializer getEventSerializer() {
692         return eventSerializer;
693     }
694 }