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.core.rolling.helper;
015
016import java.time.Instant;
017import java.util.Date;
018import java.util.HashMap;
019import java.util.Map;
020import java.util.function.Supplier;
021
022import ch.qos.logback.core.Context;
023import ch.qos.logback.core.pattern.Converter;
024import ch.qos.logback.core.pattern.ConverterUtil;
025import ch.qos.logback.core.pattern.DynamicConverter;
026import ch.qos.logback.core.pattern.LiteralConverter;
027import ch.qos.logback.core.pattern.parser.Node;
028import ch.qos.logback.core.pattern.parser.Parser;
029import ch.qos.logback.core.spi.ScanException;
030import ch.qos.logback.core.pattern.util.AlmostAsIsEscapeUtil;
031import ch.qos.logback.core.spi.ContextAwareBase;
032
033/**
034 * After parsing file name patterns, given a number or a date, instances of this
035 * class can be used to compute a file name according to the file name pattern
036 * and the current date or integer.
037 * 
038 * @author Ceki Gülcü
039 * 
040 */
041public class FileNamePattern extends ContextAwareBase {
042
043    static final Map<String, Supplier<DynamicConverter>> CONVERTER_MAP = new HashMap<>();
044    static {
045        CONVERTER_MAP.put(IntegerTokenConverter.CONVERTER_KEY, IntegerTokenConverter::new);
046        CONVERTER_MAP.put(DateTokenConverter.CONVERTER_KEY, DateTokenConverter::new);
047    }
048
049    // characters with a special meaning in regular expressions outside of character classes
050    static final String REGEX_META_CHARACTERS = "\\.[]{}()*+?^$|";
051
052    String pattern;
053    Converter<Object> headTokenConverter;
054
055    public FileNamePattern(String patternArg, Context contextArg) {
056        // the pattern is slashified
057        setPattern(FileFilterUtil.slashify(patternArg));
058        setContext(contextArg);
059        parse();
060        ConverterUtil.startConverters(this.headTokenConverter);
061    }
062
063    void parse() {
064        try {
065            // http://jira.qos.ch/browse/LOGBACK-197
066            // we escape ')' for parsing purposes. Note that the original pattern is
067            // preserved
068            // because it is shown to the user in status messages. We don't want the escaped
069            // version
070            // to leak out.
071            String patternForParsing = escapeRightParantesis(pattern);
072            Parser<Object> p = new Parser<Object>(patternForParsing, new AlmostAsIsEscapeUtil());
073            p.setContext(context);
074            Node t = p.parse();
075            this.headTokenConverter = p.compile(t, CONVERTER_MAP);
076
077        } catch (ScanException sce) {
078            addError("Failed to parse pattern \"" + pattern + "\".", sce);
079        }
080    }
081
082    String escapeRightParantesis(String in) {
083        return pattern.replace(")", "\\)");
084    }
085
086    public String toString() {
087        return pattern;
088    }
089
090    @Override
091    public int hashCode() {
092        final int prime = 31;
093        int result = 1;
094        result = prime * result + ((pattern == null) ? 0 : pattern.hashCode());
095        return result;
096    }
097
098    @Override
099    public boolean equals(Object obj) {
100        if (this == obj)
101            return true;
102        if (obj == null)
103            return false;
104        if (getClass() != obj.getClass())
105            return false;
106        FileNamePattern other = (FileNamePattern) obj;
107        if (pattern == null) {
108            if (other.pattern != null)
109                return false;
110        } else if (!pattern.equals(other.pattern))
111            return false;
112        return true;
113    }
114
115    public DateTokenConverter<Object> getPrimaryDateTokenConverter() {
116        Converter<Object> p = headTokenConverter;
117
118        while (p != null) {
119            if (p instanceof DateTokenConverter) {
120                DateTokenConverter<Object> dtc = (DateTokenConverter<Object>) p;
121                // only primary converters should be returned as
122                if (dtc.isPrimary())
123                    return dtc;
124            }
125
126            p = p.getNext();
127        }
128
129        return null;
130    }
131
132    public IntegerTokenConverter getIntegerTokenConverter() {
133        Converter<Object> p = headTokenConverter;
134
135        while (p != null) {
136            if (p instanceof IntegerTokenConverter) {
137                return (IntegerTokenConverter) p;
138            }
139
140            p = p.getNext();
141        }
142        return null;
143    }
144
145    public boolean hasIntegerTokenCOnverter() {
146        IntegerTokenConverter itc = getIntegerTokenConverter();
147        return itc != null;
148    }
149
150    public String convertMultipleArguments(Object... objectList) {
151        StringBuilder buf = new StringBuilder();
152        Converter<Object> c = headTokenConverter;
153        while (c != null) {
154            if (c instanceof MonoTypedConverter) {
155                MonoTypedConverter monoTyped = (MonoTypedConverter) c;
156                for (Object o : objectList) {
157                    if (monoTyped.isApplicable(o)) {
158                        buf.append(c.convert(o));
159                    }
160                }
161            } else {
162                buf.append(c.convert(objectList));
163            }
164            c = c.getNext();
165        }
166        return buf.toString();
167    }
168
169    public String convert(Object o) {
170        StringBuilder buf = new StringBuilder();
171        Converter<Object> p = headTokenConverter;
172        while (p != null) {
173            buf.append(p.convert(o));
174            p = p.getNext();
175        }
176        return buf.toString();
177    }
178
179    public String convertInt(int i) {
180        return convert(i);
181    }
182
183    public void setPattern(String pattern) {
184        if (pattern != null) {
185            // Trailing spaces in the pattern are assumed to be undesired.
186            this.pattern = pattern.trim();
187        }
188    }
189
190    public String getPattern() {
191        return pattern;
192    }
193
194    /**
195     * Given date, convert this instance to a regular expression.
196     *
197     * <p>Used to compute sub-regex when the pattern has both %d and %i, and the date
198     * is known.</p>
199     *
200     * <p>Implemented by delegating to {@link #toRegexForFixedDate(Instant)}.</p>
201     *
202     * @param date - known date
203     */
204    public String toRegexForFixedDate(Date date) {
205        return toRegexForFixedDate(date.toInstant());
206    }
207
208    /**
209     * Given date, convert this instance to a regular expression.
210     *
211     * Used to compute sub-regex when the pattern has both %d and %i, and the date
212     * is known.
213     *
214     * @param instant - known instant
215     */
216    public String toRegexForFixedDate(Instant instant) {
217        StringBuilder buf = new StringBuilder();
218        Converter<Object> p = headTokenConverter;
219        while (p != null) {
220            if (p instanceof LiteralConverter) {
221                buf.append(escapeRegexMetaCharacters(p.convert(null)));
222            } else if (p instanceof IntegerTokenConverter) {
223                buf.append("(\\d+)");
224            } else if (p instanceof DateTokenConverter) {
225                buf.append(escapeRegexMetaCharacters(p.convert(instant)));
226            }
227            p = p.getNext();
228        }
229        return buf.toString();
230    }
231
232
233    /**
234     * Given date, convert this instance to a regular expression
235     */
236    public String toRegex() {
237        StringBuilder buf = new StringBuilder();
238        Converter<Object> p = headTokenConverter;
239        while (p != null) {
240            if (p instanceof LiteralConverter) {
241                buf.append(escapeRegexMetaCharacters(p.convert(null)));
242            } else if (p instanceof IntegerTokenConverter) {
243                buf.append("\\d+");
244            } else if (p instanceof DateTokenConverter) {
245                DateTokenConverter<Object> dtc = (DateTokenConverter<Object>) p;
246                buf.append(dtc.toRegex());
247            }
248            p = p.getNext();
249        }
250        return buf.toString();
251    }
252
253    /**
254     * Escapes the characters of a literal part of this pattern which have a special
255     * meaning in regular expressions, e.g. '(' in "C:/Users/John(Doe)/app.log".
256     *
257     * <p>Characters are escaped one by one instead of using {@link java.util.regex.Pattern#quote(String)}
258     * because callers keep only the part of the regex after the last '/', see
259     * {@link FileFilterUtil#afterLastSlash(String)}.</p>
260     */
261    static String escapeRegexMetaCharacters(String literal) {
262        StringBuilder buf = new StringBuilder(literal.length());
263        for (int i = 0; i < literal.length(); i++) {
264            char c = literal.charAt(i);
265            if (REGEX_META_CHARACTERS.indexOf(c) != -1) {
266                buf.append('\\');
267            }
268            buf.append(c);
269        }
270        return buf.toString();
271    }
272}