001/*
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v2.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014package ch.qos.logback.classic.model.processor;
015
016import java.util.LinkedHashMap;
017import java.util.LinkedHashSet;
018import java.util.Map;
019import java.util.Set;
020import java.util.regex.Pattern;
021
022import ch.qos.logback.classic.AsyncAppender;
023import ch.qos.logback.classic.net.SMTPAppender;
024import ch.qos.logback.classic.net.SocketAppender;
025import ch.qos.logback.core.Context;
026import ch.qos.logback.core.model.AppenderModel;
027import ch.qos.logback.core.model.ImplicitModel;
028import ch.qos.logback.core.model.Model;
029import ch.qos.logback.core.model.processor.ModelHandlerBase;
030import ch.qos.logback.core.model.processor.ModelHandlerException;
031import ch.qos.logback.core.model.processor.ModelInterpretationContext;
032import ch.qos.logback.core.model.processor.PhaseIndicator;
033import ch.qos.logback.core.model.processor.ProcessingPhase;
034import ch.qos.logback.core.util.OptionHelper;
035
036/**
037 * Dependency-analysis pass over every {@link AppenderModel}: records which
038 * appenders suppress caller data ({@link AsyncAppender}, {@link SocketAppender}
039 * or {@link SMTPAppender} with {@code includeCallerData=false} / default) and
040 * which appenders need it (pattern contains a caller-data converter).
041 *
042 * <p>{@link SMTPAppender} is special: it both preprocesses caller data
043 * ({@code includeCallerData}) and formats events via its own layout. Those two
044 * contributions are recorded as separate map entries under
045 * {@code name + }{@link #INCLUDE_CALLER_DATA_NAME_SUFFIX} and
046 * {@code name + }{@link #LAYOUT_NAME_SUFFIX} so that contradictions within a
047 * single SMTP appender (e.g. includeCallerData=false but layout uses
048 * {@code %C}) can be detected.</p>
049 *
050 * <p>Analysis is skipped when the variable
051 * {@value #SKIP_CALLER_CONTRADICTION_ANALYSIS_PROPERTY} is set to
052 * {@code true} (context property, local property, system property or
053 * environment variable).</p>
054 *
055 * <p>The contradiction check is performed by {@link CallerContradictionWarnAnalyser}
056 * in its {@code postHandle()} on the enclosing {@code ConfigurationModel}, after
057 * all appender models have been visited.</p>
058 *
059 * @since 1.6.2
060 * @see CallerContradictionWarnAnalyser
061 */
062@PhaseIndicator(phase = ProcessingPhase.DEPENDENCY_ANALYSIS)
063public class CallerContradictionAnalyser extends ModelHandlerBase {
064
065    static final String APPENDER_TO_CALLER_INSTRUCTION_MAP_KEY = "APPENDER_TO_CALLER_INSTRUCTION_MAP_KEY";
066
067    /**
068     * When this property/variable is {@code true}, caller-contradiction analysis
069     * is not performed.
070     */
071    public static final String SKIP_CALLER_CONTRADICTION_ANALYSIS_PROPERTY =
072            "logback.skipCallerContradictionAnalysis";
073
074    /**
075     * Map-key suffix for an SMTPAppender {@code includeCallerData} contribution.
076     */
077    static final String INCLUDE_CALLER_DATA_NAME_SUFFIX = ".includeCallerData";
078
079    /**
080     * Map-key suffix for an SMTPAppender layout pattern contribution.
081     */
082    static final String LAYOUT_NAME_SUFFIX = ".layout";
083
084    /**
085     * Matches caller-data converter words in a logback pattern string.
086     * Single-char forms (%C class, %M method, %L line, %F file, %l location) are
087     * case-sensitive; multi-char aliases (class, method, line, file, caller) are
088     * case-insensitive and unique enough to match without case sensitivity issues.
089     * Negative lookahead prevents partial matches like %Msg being flagged.
090     */
091    static final Pattern CALLER_PATTERN = Pattern.compile(
092            "%([CMLFl]|caller|class|method|line|file)(?![a-zA-Z])");
093
094    public CallerContradictionAnalyser(Context context) {
095        super(context);
096    }
097
098    @Override
099    protected Class<AppenderModel> getSupportedModelClass() {
100        return AppenderModel.class;
101    }
102
103    @Override
104    public void handle(ModelInterpretationContext mic, Model model) throws ModelHandlerException {
105        if (isSkipCallerContradictionAnalysis(mic)) {
106            return;
107        }
108
109        AppenderModel appenderModel = (AppenderModel) model;
110
111        Map<String, CallerInstructionLogic.Instruction> appenderNameToCallerInstructionMap
112                = getAppenderNameToCallerInstructionMap(mic);
113
114        String originalClassName = appenderModel.getClassName();
115        String className = mic.getImport(originalClassName);
116        String appenderName = mic.subst(appenderModel.getName());
117
118        if (SMTPAppender.class.getName().equals(className)) {
119            recordSmtpAppenderInstructions(mic, appenderModel, appenderName,
120                    appenderNameToCallerInstructionMap);
121            return;
122        }
123
124        if (isCallerDataPreprocessingAppender(className)) {
125            if (isIncludeCallerDataTrue(mic, appenderModel)) {
126                appenderNameToCallerInstructionMap.put(appenderName,
127                        CallerInstructionLogic.Instruction.PREPROCESS_WANT);
128            } else {
129                appenderNameToCallerInstructionMap.put(appenderName,
130                        CallerInstructionLogic.Instruction.DO_NOT_WANT);
131            }
132        }
133
134        if (hasCallerDataConverters(appenderModel)) {
135            appenderNameToCallerInstructionMap.put(appenderName, CallerInstructionLogic.Instruction.DIRECT_WANT);
136        }
137    }
138
139    /**
140     * Returns {@code true} when {@value #SKIP_CALLER_CONTRADICTION_ANALYSIS_PROPERTY}
141     * resolves to {@code true}. Lookup order is local interpretation properties,
142     * context properties, system properties, then environment variables.
143     */
144    static boolean isSkipCallerContradictionAnalysis(ModelInterpretationContext mic) {
145        String value = OptionHelper.propertyLookup(SKIP_CALLER_CONTRADICTION_ANALYSIS_PROPERTY, mic,
146                mic.getContext());
147        return OptionHelper.toBoolean(value, false);
148    }
149
150    /**
151     * Records SMTPAppender contributions as two distinct instructions so that
152     * {@code includeCallerData} and the layout pattern can contradict each other.
153     * <p>
154     * The subject pattern is intentionally ignored; only the layout subtree is
155     * considered for {@link CallerInstructionLogic.Instruction#DIRECT_WANT}.
156     * </p>
157     */
158    private void recordSmtpAppenderInstructions(ModelInterpretationContext mic, AppenderModel appenderModel,
159            String appenderName, Map<String, CallerInstructionLogic.Instruction> map) {
160        String includeCallerDataKey = appenderName + INCLUDE_CALLER_DATA_NAME_SUFFIX;
161        if (isIncludeCallerDataTrue(mic, appenderModel)) {
162            map.put(includeCallerDataKey, CallerInstructionLogic.Instruction.PREPROCESS_WANT);
163        } else {
164            map.put(includeCallerDataKey, CallerInstructionLogic.Instruction.DO_NOT_WANT);
165        }
166
167        // Subject is not scanned — only layout patterns contribute DIRECT_WANT.
168        Model layoutModel = findLayoutSubModel(appenderModel);
169        if (layoutModel != null && hasCallerDataConvertersIn(layoutModel)) {
170            map.put(appenderName + LAYOUT_NAME_SUFFIX, CallerInstructionLogic.Instruction.DIRECT_WANT);
171        }
172    }
173
174    private Model findLayoutSubModel(AppenderModel appenderModel) {
175        for (Model child : appenderModel.getSubModels()) {
176            if ("layout".equalsIgnoreCase(child.getTag())) {
177                return child;
178            }
179        }
180        return null;
181    }
182
183    /**
184     * Appenders that optionally extract caller data before deferred processing
185     * or serialization, controlled by the {@code includeCallerData} property
186     * (default {@code false}). {@link SMTPAppender} is handled separately.
187     */
188    private boolean isCallerDataPreprocessingAppender(String className) {
189        return AsyncAppender.class.getName().equals(className)
190                || SocketAppender.class.getName().equals(className);
191    }
192
193    /**
194     * Note that includeCallerData is false by default on AsyncAppender,
195     * SocketAppender and SMTPAppender, so if the tag is absent we treat it as
196     * false.
197     *
198     * @param mic
199     * @param appenderModel
200     * @return
201     */
202    private boolean isIncludeCallerDataTrue(ModelInterpretationContext mic,
203            AppenderModel appenderModel) {
204        for (Model child : appenderModel.getSubModels()) {
205            if (child instanceof ImplicitModel
206                    && "includeCallerData".equalsIgnoreCase(child.getTag())) {
207                String value = mic.subst(((ImplicitModel) child).getBodyText());
208                return "true".equalsIgnoreCase(value);
209            }
210        }
211        return false; // absent → default false
212    }
213
214    private boolean hasCallerDataConverters(AppenderModel appenderModel) {
215        return hasCallerDataConvertersIn(appenderModel);
216    }
217
218    private boolean hasCallerDataConvertersIn(Model model) {
219        return collectPatternBodyTexts(model).stream()
220                .anyMatch(p -> CALLER_PATTERN.matcher(p).find());
221    }
222
223    private Set<String> collectPatternBodyTexts(Model model) {
224        Set<String> patterns = new LinkedHashSet<>();
225        collectPatternBodyTextsRecursive(model, patterns);
226        return patterns;
227    }
228
229    private void collectPatternBodyTextsRecursive(Model model, Set<String> out) {
230        if (model instanceof ImplicitModel && "pattern".equalsIgnoreCase(model.getTag())) {
231            String body = model.getBodyText();
232            if (body != null) {
233                out.add(body);
234            }
235        }
236        for (Model child : model.getSubModels()) {
237            collectPatternBodyTextsRecursive(child, out);
238        }
239    }
240
241    @SuppressWarnings("unchecked")
242    static Map<String, CallerInstructionLogic.Instruction> getAppenderNameToCallerInstructionMap(ModelInterpretationContext mic) {
243        Map<String, CallerInstructionLogic.Instruction> map =
244                (Map<String, CallerInstructionLogic.Instruction>) mic.getObjectMap().get(APPENDER_TO_CALLER_INSTRUCTION_MAP_KEY);
245        if (map == null) {
246            map = new LinkedHashMap<>();
247            mic.getObjectMap().put(APPENDER_TO_CALLER_INSTRUCTION_MAP_KEY, map);
248        }
249        return map;
250    }
251}