001/*
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 *  Copyright (C) 1999-2025, 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 v1.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 */
014
015package ch.qos.logback.core.model.processor;
016
017import ch.qos.logback.core.Context;
018import ch.qos.logback.core.FileAppender;
019import ch.qos.logback.core.model.AppenderModel;
020import ch.qos.logback.core.model.ImplicitModel;
021import ch.qos.logback.core.model.Model;
022import ch.qos.logback.core.rolling.RollingFileAppender;
023import ch.qos.logback.core.rolling.helper.FileNamePattern;
024
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Optional;
029import java.util.function.Supplier;
030import java.util.stream.Collectors;
031import java.util.stream.Stream;
032
033@PhaseIndicator(phase = ProcessingPhase.DEPENDENCY_ANALYSIS)
034public class FileCollisionAnalyser extends ModelHandlerBase {
035
036    // Key: appender name, Value: file path
037    final static String FA_FILE_COLLISION_MAP_KEY = "FA_FILE_COLLISION_MAP_KEY";
038
039    // Key: appender name, Value: FileNamePattern
040    Map<String, FileNamePattern> RFA_FILENAME_COLLISTION_MAP = new HashMap<>();
041
042
043    public FileCollisionAnalyser(Context context) {
044        super(context);
045    }
046
047    @Override
048    protected Class<AppenderModel> getSupportedModelClass() {
049        return AppenderModel.class;
050    }
051
052
053    @Override
054    public void handle(ModelInterpretationContext mic, Model model) throws ModelHandlerException {
055        AppenderModel appenderModel = (AppenderModel) model;
056
057        String originalClassName = appenderModel.getClassName();
058        String className = mic.getImport(originalClassName);
059
060        String appenderName = appenderModel.getName();
061
062        if (!fileAppenderOrRollingFileAppender(className)) {
063            return;
064        }
065
066        String tagName0 = "file";
067        checkForCollisions(mic, MapKey.FILE_COLLISION_MAP_KEY, appenderModel, appenderName, tagName0);
068
069        String tagName1 = "fileNamePattern";
070        checkForCollisions(mic, MapKey.RFA_FILENAME_COLLISION_MAP, appenderModel, appenderName, tagName1);
071    }
072
073    private static boolean fileAppenderOrRollingFileAppender(String className) {
074        return FileAppender.class.getName().equals(className) || RollingFileAppender.class.getName().equals(className);
075    }
076
077
078    boolean tagPredicate(Model model, String tagName) {
079        return (model instanceof ImplicitModel) && tagName.equals(model.getTag());
080    }
081
082    enum MapKey {
083        FILE_COLLISION_MAP_KEY, RFA_FILENAME_COLLISION_MAP
084    }
085
086    private void checkForCollisions(ModelInterpretationContext mic, MapKey mapKey, AppenderModel appenderModel, String appenderName, final String tagName) {
087
088
089        Stream<Model> streamLevel1 = appenderModel.getSubModels().stream();
090        Stream<Model> streamLevel2 = appenderModel.getSubModels().stream().flatMap(child -> child.getSubModels().stream());
091
092        List<Model> matchingModels = Stream.concat(streamLevel1, streamLevel2).filter(m -> tagPredicate(m, tagName)).collect(Collectors.toList());
093
094        //List<Model> matchingModels = appenderModel.getSubModels().stream().filter(m -> tagPredicate(m, tagName)).collect(Collectors.toList());
095
096        if(!matchingModels.isEmpty()) {
097            ImplicitModel implicitModel = (ImplicitModel) matchingModels.get(0);
098            String bodyValue = mic.subst(implicitModel.getBodyText());
099
100
101            Map<String, String> faileCollisionMap = getCollisionMapByKey(mic, mapKey);
102
103            Optional<Map.Entry<String, String>> collision = faileCollisionMap.entrySet()
104                    .stream()
105                    .filter(entry -> bodyValue.equals(entry.getValue()))
106                    .findFirst();
107
108            if (collision.isPresent()) {
109                addErrorForCollision(tagName, appenderName, collision.get().getKey(), bodyValue);
110                appenderModel.markAsHandled();
111                appenderModel.deepMarkAsSkipped();
112            } else {
113                // add to collision map if and only if no collision detected
114                // reasoning: single entry is as effective as multiple entries for collision detection
115                faileCollisionMap.put(appenderName, bodyValue);
116            }
117        }
118    }
119
120    private Map<String, String> getCollisionMapByKey(ModelInterpretationContext mic, MapKey mapKey) {
121        Map<String, String> map = (Map<String, String>) mic.getObjectMap().get(mapKey.name());
122        if(map == null) {
123            map = new HashMap<>();
124            mic.getObjectMap().put(mapKey.name(), map);
125        }
126        return map;
127    }
128
129
130    static public final String COLLISION_DETECTED = "Collision detected. Skipping initialization of appender named [%s]";
131    static public final String COLLISION_MESSAGE = "In appender [%s] option '%s' has the same value '%s' as that set for appender [%s] defined earlier";
132    private void addErrorForCollision(String optionName, String appenderName, String previousAppenderName, String optionValue) {
133        addError(String.format(COLLISION_DETECTED, appenderName));
134        addError(String.format(COLLISION_MESSAGE, appenderName, optionName, optionValue, previousAppenderName));
135    }
136}