1   /*
2    * Logback: the reliable, generic, fast and flexible logging framework.
3    * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
4    *
5    * This program and the accompanying materials are dual-licensed under
6    * either the terms of the Eclipse Public License v2.0 as published by
7    * the Eclipse Foundation
8    *
9    *   or (per the licensee's choosing)
10   *
11   * under the terms of the GNU Lesser General Public License version 2.1
12   * as published by the Free Software Foundation.
13   */
14  
15  package ch.qos.logback.core.model.processor;
16  
17  import ch.qos.logback.core.Context;
18  import ch.qos.logback.core.FileAppender;
19  import ch.qos.logback.core.model.AppenderModel;
20  import ch.qos.logback.core.model.ImplicitModel;
21  import ch.qos.logback.core.model.Model;
22  import ch.qos.logback.core.rolling.RollingFileAppender;
23  import ch.qos.logback.core.rolling.helper.FileNamePattern;
24  
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Optional;
29  import java.util.function.Supplier;
30  import java.util.stream.Collectors;
31  import java.util.stream.Stream;
32  
33  @PhaseIndicator(phase = ProcessingPhase.DEPENDENCY_ANALYSIS)
34  public class FileCollisionAnalyser extends ModelHandlerBase {
35  
36      public FileCollisionAnalyser(Context context) {
37          super(context);
38      }
39  
40      @Override
41      protected Class<AppenderModel> getSupportedModelClass() {
42          return AppenderModel.class;
43      }
44  
45  
46      @Override
47      public void handle(ModelInterpretationContext mic, Model model) throws ModelHandlerException {
48          AppenderModel appenderModel = (AppenderModel) model;
49  
50          String originalClassName = appenderModel.getClassName();
51          String className = mic.getImport(originalClassName);
52  
53          String appenderName = appenderModel.getName();
54  
55          if (!fileAppenderOrRollingFileAppender(className)) {
56              return;
57          }
58  
59          String tagName0 = "file";
60          checkForCollisions(mic, MapKey.FILE_COLLISION_MAP_KEY, appenderModel, appenderName, tagName0);
61  
62          String tagName1 = "fileNamePattern";
63          checkForCollisions(mic, MapKey.RFA_FILENAME_COLLISION_MAP, appenderModel, appenderName, tagName1);
64      }
65  
66      private static boolean fileAppenderOrRollingFileAppender(String className) {
67          return FileAppender.class.getName().equals(className) || RollingFileAppender.class.getName().equals(className);
68      }
69  
70  
71      boolean tagPredicate(Model model, String tagName) {
72          return (model instanceof ImplicitModel) && tagName.equals(model.getTag());
73      }
74  
75      enum MapKey {
76          FILE_COLLISION_MAP_KEY, RFA_FILENAME_COLLISION_MAP
77      }
78  
79      private void checkForCollisions(ModelInterpretationContext mic, MapKey mapKey, AppenderModel appenderModel, String appenderName, final String tagName) {
80  
81  
82          Stream<Model> streamLevel1 = appenderModel.getSubModels().stream();
83          Stream<Model> streamLevel2 = appenderModel.getSubModels().stream().flatMap(child -> child.getSubModels().stream());
84  
85          List<Model> matchingModels = Stream.concat(streamLevel1, streamLevel2).filter(m -> tagPredicate(m, tagName)).collect(Collectors.toList());
86  
87          //List<Model> matchingModels = appenderModel.getSubModels().stream().filter(m -> tagPredicate(m, tagName)).collect(Collectors.toList());
88  
89          if(!matchingModels.isEmpty()) {
90              ImplicitModel implicitModel = (ImplicitModel) matchingModels.get(0);
91              String bodyValue = mic.subst(implicitModel.getBodyText());
92  
93  
94              Map<String, String> faileCollisionMap = getCollisionMapByKey(mic, mapKey);
95  
96              Optional<Map.Entry<String, String>> collision = faileCollisionMap.entrySet()
97                      .stream()
98                      .filter(entry -> bodyValue.equals(entry.getValue()))
99                      .findFirst();
100 
101             if (collision.isPresent()) {
102                 addErrorForCollision(tagName, appenderName, collision.get().getKey(), bodyValue);
103                 appenderModel.markAsHandled();
104                 appenderModel.deepMarkAsSkipped();
105             } else {
106                 // add to collision map if and only if no collision detected
107                 // reasoning: single entry is as effective as multiple entries for collision detection
108                 faileCollisionMap.put(appenderName, bodyValue);
109             }
110         }
111     }
112 
113     private Map<String, String> getCollisionMapByKey(ModelInterpretationContext mic, MapKey mapKey) {
114         Map<String, String> map = (Map<String, String>) mic.getObjectMap().get(mapKey.name());
115         if(map == null) {
116             map = new HashMap<>();
117             mic.getObjectMap().put(mapKey.name(), map);
118         }
119         return map;
120     }
121 
122 
123     static public final String COLLISION_DETECTED = "Collision detected. Skipping initialization of appender named [%s]";
124     static public final String COLLISION_MESSAGE = "In appender [%s] option '%s' has the same value '%s' as that set for appender [%s] defined earlier";
125     private void addErrorForCollision(String optionName, String appenderName, String previousAppenderName, String optionValue) {
126         addError(String.format(COLLISION_DETECTED, appenderName));
127         addError(String.format(COLLISION_MESSAGE, appenderName, optionName, optionValue, previousAppenderName));
128     }
129 }