All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nl.basjes.parse.httpdlog.dissectors.StrfTimeToDateTimeFormatter Maven / Gradle / Ivy

/*
 * Apache HTTPD & NGINX Access log parsing made easy
 * Copyright (C) 2011-2018 Niels Basjes
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package nl.basjes.parse.httpdlog.dissectors;

import nl.basjes.parse.strftime.StrfTimeBaseListener;
import nl.basjes.parse.strftime.StrfTimeLexer;
import nl.basjes.parse.strftime.StrfTimeParser;
import org.antlr.v4.runtime.ANTLRErrorListener;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CodePointCharStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.atn.ATNConfigSet;
import org.antlr.v4.runtime.dfa.DFA;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.SignStyle;
import java.time.format.TextStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.WeekFields;
import java.util.BitSet;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public final class StrfTimeToDateTimeFormatter extends StrfTimeBaseListener implements ANTLRErrorListener {

    private static final Logger LOG = LoggerFactory.getLogger(StrfTimeToDateTimeFormatter.class);

    private static final WeekFields LOCAL_WEEK_FIELDS = WeekFields.of(Locale.getDefault());

    public static DateTimeFormatter convert(String strfformat) {
        return convert(strfformat, ZoneOffset.UTC);
    }

    public static DateTimeFormatter convert(String strfformat, ZoneId defaultZone) {
        CodePointCharStream input = CharStreams.fromString(strfformat);
        StrfTimeLexer lexer = new StrfTimeLexer(input);

        CommonTokenStream tokens = new CommonTokenStream(lexer);

        StrfTimeParser parser = new StrfTimeParser(tokens);

        lexer.removeErrorListeners();
        parser.removeErrorListeners();

        ParseTreeWalker walker = new ParseTreeWalker(); // create standard walker
        StrfTimeToDateTimeFormatter converter = new StrfTimeToDateTimeFormatter(strfformat, defaultZone);

        lexer.addErrorListener(converter);
        parser.addErrorListener(converter);

        StrfTimeParser.PatternContext pattern = parser.pattern();

        walker.walk(converter, pattern); // initiate walk of tree with listener

        if (converter.hasSyntaxError()) {
            return null;
        }

        return converter.build();
    }

    private String strfformat;
    private DateTimeFormatterBuilder builder;
    private ZoneId defaultZone;
    private boolean zoneWasSpecified = false;

    private StrfTimeToDateTimeFormatter(String inputStrfformat, ZoneId newDefaultZone) {
        strfformat = inputStrfformat;
        defaultZone = newDefaultZone;
        builder = new DateTimeFormatterBuilder()
            .parseCaseInsensitive();
    }

    public DateTimeFormatter build() {
        DateTimeFormatter dateTimeFormatter = builder.toFormatter();
        if (!zoneWasSpecified) {
            dateTimeFormatter = dateTimeFormatter.withZone(defaultZone);
            LOG.warn("The timestamp format \"{}\" does NOT contain a timezone so we assume \"{}\".",
                strfformat, defaultZone.getDisplayName(TextStyle.SHORT, Locale.ENGLISH));
        }
        return dateTimeFormatter;
    }

    // ------------- Error handling --------------
    private boolean syntaxError = false;

    public boolean hasSyntaxError() {
        return syntaxError;
    }

    @Override
    public void syntaxError(Recognizer recognizer, Object o, int i, int i1, String s, RecognitionException e) {
        syntaxError = true;
    }

    @Override
    public void reportAmbiguity(org.antlr.v4.runtime.Parser parser, DFA dfa, int i, int i1, boolean b, BitSet bitSet, ATNConfigSet atnConfigSet) {

    }

    @Override
    public void reportAttemptingFullContext(org.antlr.v4.runtime.Parser parser, DFA dfa, int i, int i1, BitSet bitSet, ATNConfigSet atnConfigSet) {

    }

    @Override
    public void reportContextSensitivity(org.antlr.v4.runtime.Parser parser, DFA dfa, int i, int i1, int i2, ATNConfigSet atnConfigSet) {

    }

    public static class UnsupportedStrfField extends RuntimeException {
        public UnsupportedStrfField(String s) {
            super("The field '" + s + "' cannot be converted towards a DateTimeFormatter field.");
        }
    }

    // ------------- Mapping --------------

    @Override
    public void enterMsecFrac(StrfTimeParser.MsecFracContext ctx) {
        // Apache HTTPD specific: milliseconds fraction
        builder.appendValue(ChronoField.MILLI_OF_SECOND, 3);
    }

    @Override
    public void enterUsecFrac(StrfTimeParser.UsecFracContext ctx) {
        // Apache HTTPD specific: microseconds fraction
        builder.appendValue(ChronoField.MICRO_OF_SECOND, 6);
    }

    @Override
    public void enterText(StrfTimeParser.TextContext ctx) {
        builder.appendLiteral(ctx.getText());
    }

    @Override
    public void enterTab(StrfTimeParser.TabContext ctx) {
        builder.appendLiteral('\t');
    }

    @Override
    public void enterPercent(StrfTimeParser.PercentContext ctx) {
        builder.appendLiteral('%');
    }

    @Override
    public void enterNewline(StrfTimeParser.NewlineContext ctx) {
        builder.appendLiteral('\n');
    }

    @Override
    public void enterPa(StrfTimeParser.PaContext ctx) {
        // %a   The abbreviated name of the day of the week according to the current locale.
        builder.appendText(ChronoField.DAY_OF_WEEK, TextStyle.SHORT);
    }

    @Override
    public void enterPA(StrfTimeParser.PAContext ctx) {
        // %A   The full name of the day of the week according to the current locale.
        builder.appendText(ChronoField.DAY_OF_WEEK, TextStyle.FULL);
    }

    @Override
    public void enterPb(StrfTimeParser.PbContext ctx) {
        // %b   The abbreviated month name according to the current locale.
        // %h   Equivalent to %b.
        builder.appendText(ChronoField.MONTH_OF_YEAR, TextStyle.SHORT);
    }

    @Override
    public void enterPB(StrfTimeParser.PBContext ctx) {
        // %B   The full month name according to the current locale.
        builder.appendText(ChronoField.MONTH_OF_YEAR, TextStyle.FULL);
    }

    @Override
    public void enterPc(StrfTimeParser.PcContext ctx) {
        // %c   The preferred date and time representation for the current locale.
        throw new UnsupportedStrfField("%c   The preferred date and time representation for the current locale.");
    }

    @Override
    public void enterPC(StrfTimeParser.PCContext ctx) {
        throw new UnsupportedStrfField("%C   The century number (year/100) as a 2-digit integer.");
    }

    @Override
    public void enterPd(StrfTimeParser.PdContext ctx) {
        // %d   The day of the month as a decimal number (range 01 to 31).
        builder.appendValue(ChronoField.DAY_OF_MONTH, 2);
    }

    @Override
    public void enterPD(StrfTimeParser.PDContext ctx) {
        // %D   Equivalent to %m/%d/%y. (Yecch—for Americans only)
        builder
            .appendValue(ChronoField.MONTH_OF_YEAR, 2)
            .appendLiteral('/')
            .appendValue(ChronoField.DAY_OF_MONTH, 2)
            .appendLiteral('/')
            .appendValueReduced(ChronoField.YEAR, 2, 2, 2000);
    }

    @Override
    public void enterPe(StrfTimeParser.PeContext ctx) {
        // %e   Like %d, the day of the month as a decimal number, but a leading zero is replaced by a space.
        builder.padNext(2, ' ').appendValue(ChronoField.DAY_OF_MONTH);
    }

    @Override
    public void enterPF(StrfTimeParser.PFContext ctx) {
        // %F   Equivalent to %Y-%m-%d (the ISO 8601 date format).
        builder
            .appendValue(ChronoField.YEAR, 4)
            .appendLiteral('-')
            .appendValue(ChronoField.MONTH_OF_YEAR, 2)
            .appendLiteral('-')
            .appendValue(ChronoField.DAY_OF_MONTH, 2);
    }

    @Override
    public void enterPG(StrfTimeParser.PGContext ctx) {
        // %G   The ISO 8601 week-based year (see NOTES) with century as a decimal number.
        //      The 4-digit year corresponding to the ISO week number (see %V).
        //      This has the same format and value as %Y, except that if the ISO week number
        //      belongs to the previous or next year, that year is used instead.
        builder.appendValue(LOCAL_WEEK_FIELDS.weekBasedYear(), 4);
    }

    @Override
    public void enterPg(StrfTimeParser.PgContext ctx) {
        // %g   Like %G, but without century, that is, with a 2-digit year (00–99).
        builder.appendValueReduced(LOCAL_WEEK_FIELDS.weekBasedYear(), 2, 2, 2000);
    }

    @Override
    public void enterPH(StrfTimeParser.PHContext ctx) {
        // %H   The hour as a decimal number using a 24-hour clock (range 00 to 23).
        builder.appendValue(ChronoField.CLOCK_HOUR_OF_DAY, 2);
    }

    @Override
    public void enterPI(StrfTimeParser.PIContext ctx) {
        // %I   The hour as a decimal number using a 12-hour clock (range 01 to 12).
        builder.appendValue(ChronoField.CLOCK_HOUR_OF_AMPM, 2);
    }

    @Override
    public void enterPj(StrfTimeParser.PjContext ctx) {
        // %j   The day of the year as a decimal number (range 001 to 366).
        builder.appendValue(ChronoField.DAY_OF_YEAR, 3);
    }

    @Override
    public void enterPk(StrfTimeParser.PkContext ctx) {
        // %k   The hour (24-hour clock) as a decimal number (range 0 to 23); single digits are preceded by a blank.
        //      (See also %H)
        builder.padNext(2, ' ').appendValue(ChronoField.CLOCK_HOUR_OF_DAY);
    }

    @Override
    public void enterPl(StrfTimeParser.PlContext ctx) {
        // %l   The hour (12-hour clock) as a decimal number (range 1 to 12); single digits are preceded by a blank.
        //      (See also %I)
        builder.padNext(2, ' ').appendValue(ChronoField.CLOCK_HOUR_OF_AMPM);
    }

    @Override
    public void enterPm(StrfTimeParser.PmContext ctx) {
        // %m   The month as a decimal number (range 01 to 12).
        builder.appendValue(ChronoField.MONTH_OF_YEAR, 2);
    }

    @Override
    public void enterPM(StrfTimeParser.PMContext ctx) {
        // %M   The minute as a decimal number (range 00 to 59).
        builder.appendValue(ChronoField.MINUTE_OF_HOUR, 2);
    }

    @Override
    public void enterPp(StrfTimeParser.PpContext ctx) {
        // %p   Either "AM" or "PM" according to the given time value, or the corresponding strings for the current locale.
        // Noon is treated as "PM" and midnight as "AM".
        builder.appendText(ChronoField.AMPM_OF_DAY, TextStyle.SHORT);
    }

    private static final Map AMPM_LOWER_CASE_MAPPING = new HashMap<>();
    static {
        AMPM_LOWER_CASE_MAPPING.put(0L, "am");
        AMPM_LOWER_CASE_MAPPING.put(1L, "pm");
    }

    @Override
    public void enterPP(StrfTimeParser.PPContext ctx) {
        // %P   Like %p but in lowercase: "am" or "pm" or a corresponding string for the current locale.
        builder.appendText(ChronoField.AMPM_OF_DAY, AMPM_LOWER_CASE_MAPPING);
    }

    @Override
    public void enterPr(StrfTimeParser.PrContext ctx) {
        // %r   The time in a.m. or p.m. notation. In the POSIX locale this is equivalent to %I:%M:%S %p.
        builder
            .appendValue(ChronoField.CLOCK_HOUR_OF_AMPM, 2)
            .appendLiteral(':')
            .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
            .appendLiteral(':')
            .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
            .appendLiteral(' ')
            .appendText(ChronoField.AMPM_OF_DAY, TextStyle.SHORT);
    }

    @Override
    public void enterPR(StrfTimeParser.PRContext ctx) {
        // %R   The time in 24-hour notation (%H:%M). For a version including the seconds, see %T below.
        builder
            .appendValue(ChronoField.HOUR_OF_DAY, 2)
            .appendLiteral(':')
            .appendValue(ChronoField.MINUTE_OF_HOUR, 2);
    }

    @Override
    public void enterPs(StrfTimeParser.PsContext ctx) {
        // %s   The number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC).
        // Based upon https://stackoverflow.com/questions/36066155/datetimeformatter-for-epoch-milliseconds#answer-36069732
        builder
            .appendValue(ChronoField.INSTANT_SECONDS, 1, 19, SignStyle.NEVER);
    }

    @Override
    public void enterPS(StrfTimeParser.PSContext ctx) {
        // %S   The second as a decimal number (range 00 to 60). (The range is up to 60 to allow for occasional leap seconds)
        builder
            .appendValue(ChronoField.SECOND_OF_MINUTE, 2);
    }

    @Override
    public void enterPT(StrfTimeParser.PTContext ctx) {
        // %T   The time in 24-hour notation (%H:%M:%S).
        builder
            .appendValue(ChronoField.HOUR_OF_DAY, 2)
            .appendLiteral(':')
            .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
            .appendLiteral(':')
            .appendValue(ChronoField.SECOND_OF_MINUTE, 2);
    }

    @Override
    public void enterPu(StrfTimeParser.PuContext ctx) {
        // %u   The day of the week as a decimal, range 1 to 7, Monday being 1. See also %w.
        builder.appendValue(WeekFields.ISO.dayOfWeek(), 1);
    }

    @Override
    public void enterPU(StrfTimeParser.PUContext ctx) {
        // %U   The week number of the current year as a decimal number, range 00 to 53, starting with
        //      the first Sunday as the first day of week 01. See also %V and %W.
        throw new UnsupportedStrfField("%U The week number of the current year ... ");
    }

    @Override
    public void enterPV(StrfTimeParser.PVContext ctx) {
        // %V   The ISO 8601 week number (see NOTES) of the current year as a decimal number, range 01 to 53,
        // where week 1 is the first week that has at least 4 days in the new year. See also %U and %W.
        builder.appendValue(WeekFields.ISO.weekOfYear());
    }

    @Override
    public void enterPw(StrfTimeParser.PwContext ctx) {
        // %w   The day of the week as a decimal, range 0 to 6, Sunday being 0. See also %u.
        throw new UnsupportedStrfField("%w   The day of the week as a decimal, range 0 to 6, Sunday being 0. See also %u.");
    }

    @Override
    public void enterPW(StrfTimeParser.PWContext ctx) {
        // %W   The week number of the current year as a decimal number, range 00 to 53,
        //      starting with the first Monday as the first day of week 01.
        builder.appendValue(WeekFields.ISO.weekOfYear(), 2);
    }

    @Override
    public void enterPx(StrfTimeParser.PxContext ctx) {
        // %x   The preferred date representation for the current locale without the time.
        throw new UnsupportedStrfField("%x   The preferred date representation for the current locale without the time.");
    }

    @Override
    public void enterPX(StrfTimeParser.PXContext ctx) {
        // %X   The preferred time representation for the current locale without the date.
        throw new UnsupportedStrfField("%X   The preferred time representation for the current locale without the date.");
    }

    @Override
    public void enterPy(StrfTimeParser.PyContext ctx) {
        // %y   The year as a decimal number without a century (range 00 to 99).
        builder.appendValueReduced(ChronoField.YEAR, 2, 2, 2000);
    }

    @Override
    public void enterPY(StrfTimeParser.PYContext ctx) {
        // %Y   The year as a decimal number including the century.
        builder.appendValue(ChronoField.YEAR, 4);
    }

    @Override
    public void enterPz(StrfTimeParser.PzContext ctx) {
        // %z   The +hhmm or -hhmm numeric timezone.
        builder.appendOffset("+HHMM", "+0000");
        zoneWasSpecified = true;
    }

    @Override
    public void enterPZ(StrfTimeParser.PZContext ctx) {
        // %Z   The timezone name or abbreviation.
        builder.appendZoneText(TextStyle.SHORT);
        zoneWasSpecified = true;
    }

    @Override
    public void enterPplus(StrfTimeParser.PplusContext ctx) {
        throw new UnsupportedStrfField("%p   The date and time in date(1) format.");
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy