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}