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.appender.rolling;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.nio.file.Files;
22  import java.nio.file.Path;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.SortedMap;
28  import java.util.concurrent.TimeUnit;
29  import java.util.zip.Deflater;
30  
31  import org.apache.logging.log4j.core.Core;
32  import org.apache.logging.log4j.core.appender.rolling.action.Action;
33  import org.apache.logging.log4j.core.appender.rolling.action.CompositeAction;
34  import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction;
35  import org.apache.logging.log4j.core.appender.rolling.action.PathCondition;
36  import org.apache.logging.log4j.core.appender.rolling.action.PosixViewAttributeAction;
37  import org.apache.logging.log4j.core.config.Configuration;
38  import org.apache.logging.log4j.core.config.plugins.Plugin;
39  import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
40  import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
41  import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
42  import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
43  import org.apache.logging.log4j.core.config.plugins.PluginElement;
44  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
45  import org.apache.logging.log4j.core.lookup.StrSubstitutor;
46  import org.apache.logging.log4j.core.util.Integers;
47  
48  /**
49   * When rolling over, <code>DefaultRolloverStrategy</code> renames files according to an algorithm as described below.
50   *
51   * <p>
52   * The DefaultRolloverStrategy is a combination of a time-based policy and a fixed-window policy. When the file name
53   * pattern contains a date format then the rollover time interval will be used to calculate the time to use in the file
54   * pattern. When the file pattern contains an integer replacement token one of the counting techniques will be used.
55   * </p>
56   * <p>
57   * When the ascending attribute is set to true (the default) then the counter will be incremented and the current log
58   * file will be renamed to include the counter value. If the counter hits the maximum value then the oldest file, which
59   * will have the smallest counter, will be deleted, all other files will be renamed to have their counter decremented
60   * and then the current file will be renamed to have the maximum counter value. Note that with this counting strategy
61   * specifying a large maximum value may entirely avoid renaming files.
62   * </p>
63   * <p>
64   * When the ascending attribute is false, then the "normal" fixed-window strategy will be used.
65   * </p>
66   * <p>
67   * Let <em>max</em> and <em>min</em> represent the values of respectively the <b>MaxIndex</b> and <b>MinIndex</b>
68   * options. Let "foo.log" be the value of the <b>ActiveFile</b> option and "foo.%i.log" the value of
69   * <b>FileNamePattern</b>. Then, when rolling over, the file <code>foo.<em>max</em>.log</code> will be deleted, the file
70   * <code>foo.<em>max-1</em>.log</code> will be renamed as <code>foo.<em>max</em>.log</code>, the file
71   * <code>foo.<em>max-2</em>.log</code> renamed as <code>foo.<em>max-1</em>.log</code>, and so on, the file
72   * <code>foo.<em>min+1</em>.log</code> renamed as <code>foo.<em>min+2</em>.log</code>. Lastly, the active file
73   * <code>foo.log</code> will be renamed as <code>foo.<em>min</em>.log</code> and a new active file name
74   * <code>foo.log</code> will be created.
75   * </p>
76   * <p>
77   * Given that this rollover algorithm requires as many file renaming operations as the window size, large window sizes
78   * are discouraged.
79   * </p>
80   */
81  @Plugin(name = "DefaultRolloverStrategy", category = Core.CATEGORY_NAME, printObject = true)
82  public class DefaultRolloverStrategy extends AbstractRolloverStrategy {
83  
84      private static final int MIN_WINDOW_SIZE = 1;
85      private static final int DEFAULT_WINDOW_SIZE = 7;
86  
87      /**
88       * Builds DefaultRolloverStrategy instances.
89       */
90      public static class Builder implements org.apache.logging.log4j.core.util.Builder<DefaultRolloverStrategy> {
91          @PluginBuilderAttribute("max")
92          private String max;
93  
94          @PluginBuilderAttribute("min")
95          private String min;
96  
97          @PluginBuilderAttribute("fileIndex")
98          private String fileIndex;
99  
100         @PluginBuilderAttribute("compressionLevel")
101         private String compressionLevelStr;
102 
103         @PluginElement("Actions")
104         private Action[] customActions;
105 
106         @PluginBuilderAttribute(value = "stopCustomActionsOnError")
107         private boolean stopCustomActionsOnError = true;
108 
109         @PluginBuilderAttribute(value = "tempCompressedFilePattern")
110         private String tempCompressedFilePattern;
111 
112         @PluginConfiguration
113         private Configuration config;
114 
115         @Override
116         public DefaultRolloverStrategy build() {
117             int minIndex;
118             int maxIndex;
119             boolean useMax;
120 
121             if (fileIndex != null && fileIndex.equalsIgnoreCase("nomax")) {
122                 minIndex = Integer.MIN_VALUE;
123                 maxIndex = Integer.MAX_VALUE;
124                 useMax = false;
125             } else {
126                 useMax = fileIndex == null ? true : fileIndex.equalsIgnoreCase("max");
127                 minIndex = MIN_WINDOW_SIZE;
128                 if (min != null) {
129                     minIndex = Integer.parseInt(min);
130                     if (minIndex < 1) {
131                         LOGGER.error("Minimum window size too small. Limited to " + MIN_WINDOW_SIZE);
132                         minIndex = MIN_WINDOW_SIZE;
133                     }
134                 }
135                 maxIndex = DEFAULT_WINDOW_SIZE;
136                 if (max != null) {
137                     maxIndex = Integer.parseInt(max);
138                     if (maxIndex < minIndex) {
139                         maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex;
140                         LOGGER.error("Maximum window size must be greater than the minimum windows size. Set to " + maxIndex);
141                     }
142                 }
143             }
144             final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION);
145             // The config object can be null when this object is built programmatically.
146             final StrSubstitutor nonNullStrSubstitutor = config != null ? config.getStrSubstitutor() : new StrSubstitutor();
147 			return new DefaultRolloverStrategy(minIndex, maxIndex, useMax, compressionLevel, nonNullStrSubstitutor,
148                     customActions, stopCustomActionsOnError, tempCompressedFilePattern);
149         }
150 
151         public String getMax() {
152             return max;
153         }
154 
155         /**
156          * Defines the maximum number of files to keep.
157          *
158          * @param max The maximum number of files to keep.
159          * @return This builder for chaining convenience
160          */
161         public Builder withMax(final String max) {
162             this.max = max;
163             return this;
164         }
165 
166         public String getMin() {
167             return min;
168         }
169 
170         /**
171          * Defines the minimum number of files to keep.
172          *
173          * @param min The minimum number of files to keep.
174          * @return This builder for chaining convenience
175          */
176         public Builder withMin(final String min) {
177             this.min = min;
178             return this;
179         }
180 
181         public String getFileIndex() {
182             return fileIndex;
183         }
184 
185         /**
186          * Defines the file index for rolling strategy.
187          *
188          * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a smaller
189          *            index. If set to "min", file renaming and the counter will follow the Fixed Window strategy.
190          * @return This builder for chaining convenience
191          */
192         public Builder withFileIndex(final String fileIndex) {
193             this.fileIndex = fileIndex;
194             return this;
195         }
196 
197         public String getCompressionLevelStr() {
198             return compressionLevelStr;
199         }
200 
201         /**
202          * Defines compression level.
203          *
204          * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
205          * @return This builder for chaining convenience
206          */
207         public Builder withCompressionLevelStr(final String compressionLevelStr) {
208             this.compressionLevelStr = compressionLevelStr;
209             return this;
210         }
211 
212         public Action[] getCustomActions() {
213             return customActions;
214         }
215 
216         /**
217          * Defines custom actions.
218          *
219          * @param customActions custom actions to perform asynchronously after rollover
220          * @return This builder for chaining convenience
221          */
222         public Builder withCustomActions(final Action[] customActions) {
223             this.customActions = customActions;
224             return this;
225         }
226 
227         public boolean isStopCustomActionsOnError() {
228             return stopCustomActionsOnError;
229         }
230 
231         /**
232          * Defines whether to stop executing asynchronous actions if an error occurs.
233          *
234          * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
235          * @return This builder for chaining convenience
236          */
237         public Builder withStopCustomActionsOnError(final boolean stopCustomActionsOnError) {
238             this.stopCustomActionsOnError = stopCustomActionsOnError;
239             return this;
240         }
241 
242         public String getTempCompressedFilePattern() {
243             return tempCompressedFilePattern;
244         }
245 
246         /**
247          * Defines temporary compression file pattern.
248          *
249          * @param tempCompressedFilePattern File pattern of the working file pattern used during compression, if null no temporary file are used
250          * @return This builder for chaining convenience
251          */
252         public Builder withTempCompressedFilePattern(final String tempCompressedFilePattern) {
253             this.tempCompressedFilePattern = tempCompressedFilePattern;
254             return this;
255         }
256 
257         public Configuration getConfig() {
258             return config;
259         }
260 
261         /**
262          * Defines configuration.
263          *
264          * @param config The Configuration.
265          * @return This builder for chaining convenience
266          */
267         public Builder withConfig(final Configuration config) {
268             this.config = config;
269             return this;
270         }
271     }
272 
273     @PluginBuilderFactory
274     public static Builder newBuilder() {
275         return new Builder();
276     }
277 
278     /**
279      * Creates the DefaultRolloverStrategy.
280      *
281      * @param max The maximum number of files to keep.
282      * @param min The minimum number of files to keep.
283      * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a smaller
284      *            index. If set to "min", file renaming and the counter will follow the Fixed Window strategy.
285      * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
286      * @param customActions custom actions to perform asynchronously after rollover
287      * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
288      * @param config The Configuration.
289      * @return A DefaultRolloverStrategy.
290      * @deprecated Since 2.9 Usage of Builder API is preferable
291      */
292     @PluginFactory
293     @Deprecated
294     public static DefaultRolloverStrategy createStrategy(
295             // @formatter:off
296             @PluginAttribute("max") final String max,
297             @PluginAttribute("min") final String min,
298             @PluginAttribute("fileIndex") final String fileIndex,
299             @PluginAttribute("compressionLevel") final String compressionLevelStr,
300             @PluginElement("Actions") final Action[] customActions,
301             @PluginAttribute(value = "stopCustomActionsOnError", defaultBoolean = true)
302                     final boolean stopCustomActionsOnError,
303             @PluginConfiguration final Configuration config) {
304         return DefaultRolloverStrategy.newBuilder()
305                     .withMin(min)
306                     .withMax(max)
307                     .withFileIndex(fileIndex)
308                     .withCompressionLevelStr(compressionLevelStr)
309                     .withCustomActions(customActions)
310                     .withStopCustomActionsOnError(stopCustomActionsOnError)
311                     .withConfig(config)
312                 .build();
313             // @formatter:on
314     }
315 
316     /**
317      * Index for oldest retained log file.
318      */
319     private final int maxIndex;
320 
321     /**
322      * Index for most recent log file.
323      */
324     private final int minIndex;
325     private final boolean useMax;
326     private final int compressionLevel;
327     private final List<Action> customActions;
328     private final boolean stopCustomActionsOnError;
329     private final PatternProcessor tempCompressedFilePattern;
330 
331     /**
332      * Constructs a new instance.
333      *
334      * @param minIndex The minimum index.
335      * @param maxIndex The maximum index.
336      * @param customActions custom actions to perform asynchronously after rollover
337      * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
338      * @deprecated Since 2.9 Added tempCompressedFilePatternString parameter
339      */
340     @Deprecated
341     protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax,
342             final int compressionLevel, final StrSubstitutor strSubstitutor, final Action[] customActions,
343             final boolean stopCustomActionsOnError) {
344         this(minIndex, maxIndex, useMax, compressionLevel,
345                        strSubstitutor, customActions, stopCustomActionsOnError, null);
346     }
347 
348     /**
349      * Constructs a new instance.
350      *
351      * @param minIndex The minimum index.
352      * @param maxIndex The maximum index.
353      * @param customActions custom actions to perform asynchronously after rollover
354      * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
355      * @param tempCompressedFilePatternString File pattern of the working file
356      *                                     used during compression, if null no temporary file are used
357      */
358     protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax,
359             final int compressionLevel, final StrSubstitutor strSubstitutor, final Action[] customActions,
360             final boolean stopCustomActionsOnError, final String tempCompressedFilePatternString) {
361         super(strSubstitutor);
362         this.minIndex = minIndex;
363         this.maxIndex = maxIndex;
364         this.useMax = useMax;
365         this.compressionLevel = compressionLevel;
366         this.stopCustomActionsOnError = stopCustomActionsOnError;
367         this.customActions = customActions == null ? Collections.<Action> emptyList() : Arrays.asList(customActions);
368         this.tempCompressedFilePattern =
369                 tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null;
370     }
371 
372     public int getCompressionLevel() {
373         return this.compressionLevel;
374     }
375 
376     public List<Action> getCustomActions() {
377         return customActions;
378     }
379 
380     public int getMaxIndex() {
381         return this.maxIndex;
382     }
383 
384     public int getMinIndex() {
385         return this.minIndex;
386     }
387 
388     public boolean isStopCustomActionsOnError() {
389         return stopCustomActionsOnError;
390     }
391 
392     public boolean isUseMax() {
393         return useMax;
394     }
395 
396     public PatternProcessor getTempCompressedFilePattern() {
397         return tempCompressedFilePattern;
398     }
399 
400     private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) {
401         return useMax ? purgeAscending(lowIndex, highIndex, manager) : purgeDescending(lowIndex, highIndex, manager);
402     }
403 
404     /**
405      * Purges and renames old log files in preparation for rollover. The oldest file will have the smallest index, the
406      * newest the highest.
407      *
408      * @param lowIndex low index. Log file associated with low index will be deleted if needed.
409      * @param highIndex high index.
410      * @param manager The RollingFileManager
411      * @return true if purge was successful and rollover should be attempted.
412      */
413     private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
414         final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
415         final int maxFiles = highIndex - lowIndex + 1;
416 
417         boolean renameFiles = !eligibleFiles.isEmpty() && eligibleFiles.lastKey() >= maxIndex;
418         while (eligibleFiles.size() >= maxFiles) {
419             try {
420                 LOGGER.debug("Eligible files: {}", eligibleFiles);
421                 final Integer key = eligibleFiles.firstKey();
422                 LOGGER.debug("Deleting {}", eligibleFiles.get(key).toFile().getAbsolutePath());
423                 Files.delete(eligibleFiles.get(key));
424                 eligibleFiles.remove(key);
425                 renameFiles = true;
426             } catch (final IOException ioe) {
427                 LOGGER.error("Unable to delete {}, {}", eligibleFiles.firstKey(), ioe.getMessage(), ioe);
428                 break;
429             }
430         }
431         final StringBuilder buf = new StringBuilder();
432         if (renameFiles) {
433             for (final Map.Entry<Integer, Path> entry : eligibleFiles.entrySet()) {
434                 buf.setLength(0);
435                 // LOG4J2-531: directory scan & rollover must use same format
436                 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, entry.getKey() - 1);
437                 final String currentName = entry.getValue().toFile().getName();
438                 String renameTo = buf.toString();
439                 final int suffixLength = suffixLength(renameTo);
440                 if (suffixLength > 0 && suffixLength(currentName) == 0) {
441                    renameTo = renameTo.substring(0, renameTo.length() - suffixLength);
442                 }
443                 final Action action = new FileRenameAction(entry.getValue().toFile(), new File(renameTo), true);
444                 try {
445                     LOGGER.debug("DefaultRolloverStrategy.purgeAscending executing {}", action);
446                     if (!action.execute()) {
447                         return -1;
448                     }
449                 } catch (final Exception ex) {
450                     LOGGER.warn("Exception during purge in RollingFileAppender", ex);
451                     return -1;
452                 }
453             }
454         }
455 
456         return eligibleFiles.size() > 0 ?
457                 (eligibleFiles.lastKey() < highIndex ? eligibleFiles.lastKey() + 1 : highIndex) : lowIndex;
458     }
459 
460     /**
461      * Purges and renames old log files in preparation for rollover. The newest file will have the smallest index, the
462      * oldest will have the highest.
463      *
464      * @param lowIndex low index
465      * @param highIndex high index. Log file associated with high index will be deleted if needed.
466      * @param manager The RollingFileManager
467      * @return true if purge was successful and rollover should be attempted.
468      */
469     private int purgeDescending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
470         // Retrieve the files in descending order, so the highest key will be first.
471         final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager, false);
472         final int maxFiles = highIndex - lowIndex + 1;
473 
474         while (eligibleFiles.size() >= maxFiles) {
475             try {
476                 final Integer key = eligibleFiles.firstKey();
477                 Files.delete(eligibleFiles.get(key));
478                 eligibleFiles.remove(key);
479             } catch (final IOException ioe) {
480                 LOGGER.error("Unable to delete {}, {}", eligibleFiles.firstKey(), ioe.getMessage(), ioe);
481                 break;
482             }
483         }
484         final StringBuilder buf = new StringBuilder();
485         for (final Map.Entry<Integer, Path> entry : eligibleFiles.entrySet()) {
486             buf.setLength(0);
487             // LOG4J2-531: directory scan & rollover must use same format
488             manager.getPatternProcessor().formatFileName(strSubstitutor, buf, entry.getKey() + 1);
489             final String currentName = entry.getValue().toFile().getName();
490             String renameTo = buf.toString();
491             final int suffixLength = suffixLength(renameTo);
492             if (suffixLength > 0 && suffixLength(currentName) == 0) {
493                 renameTo = renameTo.substring(0, renameTo.length() - suffixLength);
494             }
495             final Action action = new FileRenameAction(entry.getValue().toFile(), new File(renameTo), true);
496             try {
497                 LOGGER.debug("DefaultRolloverStrategy.purgeDescending executing {}", action);
498                 if (!action.execute()) {
499                     return -1;
500                 }
501             } catch (final Exception ex) {
502                 LOGGER.warn("Exception during purge in RollingFileAppender", ex);
503                 return -1;
504             }
505         }
506 
507         return lowIndex;
508     }
509 
510     /**
511      * Performs the rollover.
512      *
513      * @param manager The RollingFileManager name for current active log file.
514      * @return A RolloverDescription.
515      * @throws SecurityException if an error occurs.
516      */
517     @Override
518     public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
519         int fileIndex;
520 		final StringBuilder buf = new StringBuilder(255);
521         if (minIndex == Integer.MIN_VALUE) {
522             final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
523             fileIndex = eligibleFiles.size() > 0 ? eligibleFiles.lastKey() + 1 : 1;
524 			manager.getPatternProcessor().formatFileName(strSubstitutor, buf, fileIndex);
525         } else {
526             if (maxIndex < 0) {
527                 return null;
528             }
529             final long startNanos = System.nanoTime();
530             fileIndex = purge(minIndex, maxIndex, manager);
531             if (fileIndex < 0) {
532                 return null;
533             }
534 			manager.getPatternProcessor().formatFileName(strSubstitutor, buf, fileIndex);
535             if (LOGGER.isTraceEnabled()) {
536                 final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
537                 LOGGER.trace("DefaultRolloverStrategy.purge() took {} milliseconds", durationMillis);
538             }
539         }
540 
541         final String currentFileName = manager.getFileName();
542 
543         String renameTo = buf.toString();
544         final String compressedName = renameTo;
545         Action compressAction = null;
546 
547         final FileExtension fileExtension = manager.getFileExtension();
548         if (fileExtension != null) {
549             final File renameToFile = new File(renameTo);
550             renameTo = renameTo.substring(0, renameTo.length() - fileExtension.length());
551             if (tempCompressedFilePattern != null) {
552                 buf.delete(0, buf.length());
553                 tempCompressedFilePattern.formatFileName(strSubstitutor, buf, fileIndex);
554                 final String tmpCompressedName = buf.toString();
555                 final File tmpCompressedNameFile = new File(tmpCompressedName);
556                 final File parentFile = tmpCompressedNameFile.getParentFile();
557                 if (parentFile != null) {
558                     parentFile.mkdirs();
559                 }
560                 compressAction = new CompositeAction(
561                         Arrays.asList(fileExtension.createCompressAction(renameTo, tmpCompressedName,
562                                 true, compressionLevel),
563                                 new FileRenameAction(tmpCompressedNameFile,
564                                         renameToFile, true)),
565                         true);
566             } else {
567                 compressAction = fileExtension.createCompressAction(renameTo, compressedName,
568                         true, compressionLevel);
569             }
570         }
571 
572         if (currentFileName.equals(renameTo)) {
573             LOGGER.warn("Attempt to rename file {} to itself will be ignored", currentFileName);
574             return new RolloverDescriptionImpl(currentFileName, false, null, null);
575         }
576 
577         if (compressAction != null && manager.isAttributeViewEnabled()) {
578             // Propagate posix attribute view to compressed file
579             // @formatter:off
580             final Action posixAttributeViewAction = PosixViewAttributeAction.newBuilder()
581                                                         .withBasePath(compressedName)
582                                                         .withFollowLinks(false)
583                                                         .withMaxDepth(1)
584                                                         .withPathConditions(new PathCondition[0])
585                                                         .withSubst(getStrSubstitutor())
586                                                         .withFilePermissions(manager.getFilePermissions())
587                                                         .withFileOwner(manager.getFileOwner())
588                                                         .withFileGroup(manager.getFileGroup())
589                                                         .build();
590             // @formatter:on
591             compressAction = new CompositeAction(Arrays.asList(compressAction, posixAttributeViewAction), false);
592         }
593 
594         final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo),
595                     manager.isRenameEmptyFiles());
596 
597         final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError);
598         return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction);
599     }
600 
601     @Override
602     public String toString() {
603         return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ", useMax=" + useMax + ")";
604     }
605 
606 }