1
2
3
4
5
6
7
8
9
10
11
12
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.model.SiftModel;
23 import ch.qos.logback.core.rolling.RollingFileAppender;
24 import ch.qos.logback.core.rolling.helper.FileNamePattern;
25
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Optional;
31 import java.util.function.Supplier;
32 import java.util.stream.Collectors;
33 import java.util.stream.Stream;
34
35 @PhaseIndicator(phase = ProcessingPhase.DEPENDENCY_ANALYSIS)
36 public class FileCollisionAnalyser extends ModelHandlerBase {
37
38 public FileCollisionAnalyser(Context context) {
39 super(context);
40 }
41
42 @Override
43 protected Class<AppenderModel> getSupportedModelClass() {
44 return AppenderModel.class;
45 }
46
47
48 @Override
49 public void handle(ModelInterpretationContext mic, Model model) throws ModelHandlerException {
50 AppenderModel appenderModel = (AppenderModel) model;
51
52 String originalClassName = appenderModel.getClassName();
53 String className = mic.getImport(originalClassName);
54
55 String appenderName = appenderModel.getName();
56
57
58
59
60
61
62 Model siftModel = firstSubModelOfClass(appenderModel, SiftModel.class);
63 if (siftModel != null) {
64 checkSiftingAppenderForCollision(mic, appenderModel, siftModel, appenderName);
65 return;
66 }
67
68 if (!fileAppenderOrRollingFileAppender(className)) {
69 return;
70 }
71
72 String tagName0 = "file";
73 checkForCollisions(mic, MapKey.FILE_COLLISION_MAP_KEY, appenderModel, appenderName, tagName0);
74
75 String tagName1 = "fileNamePattern";
76 checkForCollisions(mic, MapKey.RFA_FILENAME_COLLISION_MAP, appenderModel, appenderName, tagName1);
77 }
78
79 private static boolean fileAppenderOrRollingFileAppender(String className) {
80 return FileAppender.class.getName().equals(className) || RollingFileAppender.class.getName().equals(className);
81 }
82
83
84 boolean tagPredicate(Model model, String tagName) {
85 return (model instanceof ImplicitModel) && tagName.equals(model.getTag());
86 }
87
88 enum MapKey {
89 FILE_COLLISION_MAP_KEY, RFA_FILENAME_COLLISION_MAP
90 }
91
92 private void checkForCollisions(ModelInterpretationContext mic, MapKey mapKey, AppenderModel appenderModel, String appenderName, final String tagName) {
93
94
95 Stream<Model> streamLevel1 = appenderModel.getSubModels().stream();
96 Stream<Model> streamLevel2 = appenderModel.getSubModels().stream().flatMap(child -> child.getSubModels().stream());
97
98 List<Model> matchingModels = Stream.concat(streamLevel1, streamLevel2).filter(m -> tagPredicate(m, tagName)).collect(Collectors.toList());
99
100 if(!matchingModels.isEmpty()) {
101 ImplicitModel implicitModel = (ImplicitModel) matchingModels.get(0);
102 String bodyValue = mic.subst(implicitModel.getBodyText());
103
104
105 Map<String, String> collisionMap = getCollisionMapByKey(mic, mapKey);
106
107 Optional<Map.Entry<String, String>> collision = collisionMap.entrySet()
108 .stream()
109 .filter(entry -> bodyValue.equals(entry.getValue()))
110 .findFirst();
111
112 if (collision.isPresent()) {
113 addErrorForCollision(tagName, appenderName, collision.get().getKey(), bodyValue);
114 appenderModel.markAsHandled();
115 appenderModel.deepMarkAsSkipped();
116 } else {
117
118
119 collisionMap.put(appenderName, bodyValue);
120 }
121 }
122 }
123
124 private Map<String, String> getCollisionMapByKey(ModelInterpretationContext mic, MapKey mapKey) {
125 Map<String, String> map = (Map<String, String>) mic.getObjectMap().get(mapKey.name());
126 if(map == null) {
127 map = new HashMap<>();
128 mic.getObjectMap().put(mapKey.name(), map);
129 }
130 return map;
131 }
132
133
134 static public final String COLLISION_DETECTED = "Collision detected. Skipping initialization of appender named [%s]";
135 static public final String COLLISION_MESSAGE = "In appender [%s] option '%s' has the same value '%s' as that set for appender [%s] defined earlier";
136 private void addErrorForCollision(String optionName, String appenderName, String previousAppenderName, String optionValue) {
137 addError(String.format(COLLISION_DETECTED, appenderName));
138 addError(String.format(COLLISION_MESSAGE, appenderName, optionName, optionValue, previousAppenderName));
139 }
140
141 static public final String SIFT_COLLISION_MESSAGE_0 =
142 "The nested appender of SiftingAppender [%s] does not reference the discriminator key [%s] in its 'file'/'fileNamePattern'. ";
143 static public final String SIFT_COLLISION_MESSAGE_1 =
144 "Every child appender will therefore write to the same file [%s] causing collisions. ";
145 static public final String SIFT_COLLISION_MESSAGE_2 = "Consider embedding ${%s} in the file path.";
146
147
148
149
150
151
152 private void checkSiftingAppenderForCollision(ModelInterpretationContext mic, AppenderModel appenderModel,
153 Model siftModel, String appenderName) {
154
155 String discriminatorKey = findDiscriminatorKey(appenderModel);
156 if (discriminatorKey == null || discriminatorKey.isEmpty()) {
157
158
159 return;
160 }
161
162 Model nestedAppenderModel = firstSubModelOfClass(siftModel, AppenderModel.class);
163 if (nestedAppenderModel == null) {
164 return;
165 }
166
167 List<String> fileValues = new ArrayList<>();
168 collectBodyText(nestedAppenderModel, "file", fileValues);
169 collectBodyText(nestedAppenderModel, "fileNamePattern", fileValues);
170
171 if (fileValues.isEmpty()) {
172
173 return;
174 }
175
176 String keyReference = "${" + discriminatorKey;
177 boolean referencesKey = fileValues.stream().anyMatch(v -> v.contains(keyReference));
178 if (!referencesKey) {
179 String resolvedTarget = mic.subst(fileValues.get(0));
180 addWarn(String.format(SIFT_COLLISION_MESSAGE_0, appenderName, discriminatorKey));
181 addWarn(String.format(SIFT_COLLISION_MESSAGE_1, resolvedTarget));
182 addWarn(String.format(SIFT_COLLISION_MESSAGE_2, discriminatorKey));
183 }
184 }
185
186 private Model firstSubModelOfClass(Model parent, Class<? extends Model> modelClass) {
187 return parent.getSubModels().stream().filter(modelClass::isInstance).findFirst().orElse(null);
188 }
189
190 private String findDiscriminatorKey(AppenderModel appenderModel) {
191 Optional<Model> discriminator = appenderModel.getSubModels().stream()
192 .filter(m -> "discriminator".equalsIgnoreCase(m.getTag())).findFirst();
193
194 if (discriminator.isEmpty()) {
195 return null;
196 }
197
198 for (Model child : discriminator.get().getSubModels()) {
199 if ("key".equalsIgnoreCase(child.getTag())) {
200 String bodyText = child.getBodyText();
201 if (bodyText != null) {
202 return bodyText.trim();
203 }
204 }
205 }
206 return null;
207 }
208
209
210
211
212
213
214 private void collectBodyText(Model appenderModel, String tagName, List<String> out) {
215 for (Model child : appenderModel.getSubModels()) {
216
217 addBodyTextIfMatching(child, tagName, out);
218
219 for (Model grandChild : child.getSubModels()) {
220 addBodyTextIfMatching(grandChild, tagName, out);
221 }
222 }
223 }
224
225 private void addBodyTextIfMatching(Model model, String tagName, List<String> out) {
226 if (!tagPredicate(model, tagName)) {
227 return;
228 }
229 String bodyText = model.getBodyText();
230 if (bodyText != null) {
231 out.add(bodyText);
232 }
233 }
234 }