com.zulily.omicron.crontab.Crontab.java Source code

Java tutorial

Introduction

Here is the source code for com.zulily.omicron.crontab.Crontab.java

Source

/*
 * Copyright (C) 2014 zulily, Inc.
 *
 * 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 com.zulily.omicron.crontab;

import com.google.common.base.CharMatcher;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.zulily.omicron.Utils;
import com.zulily.omicron.conf.ConfigKey;
import com.zulily.omicron.conf.Configuration;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.zulily.omicron.Utils.COMMA_JOINER;
import static com.zulily.omicron.Utils.error;
import static com.zulily.omicron.Utils.info;
import static com.zulily.omicron.Utils.warn;

/**
 * This class contains the logic of reading the specified crontab into memory.
 * <p>
 * Schedule rows are read in an represented as {@link com.zulily.omicron.crontab.CrontabExpression} types
 * Override rows are read in and associated with the next row that is not blank. If the next non-blank row is commented,
 * then the override will be ignored.
 */
public final class Crontab {
    public final static String OVERRIDE_KEYWORD = "#override:";

    private final ImmutableSet<CrontabExpression> crontabExpressions;
    private final ImmutableList<CronVariable> variableList;
    private final ImmutableMap<Integer, Configuration> configurationOverrides;
    private final int badRowCount;
    private final long crontabTimestamp;

    /**
     * Constructor
     *
     * @param configuration The global configuration object for Omicron
     */
    public Crontab(final Configuration configuration) {

        checkNotNull(configuration, "configuration");

        final File crontabFile = new File(configuration.getString(ConfigKey.CrontabPath));

        checkState(Utils.fileExistsAndCanRead(crontabFile), "Cannot read/find crontab: ",
                crontabFile.getAbsolutePath());

        final List<CronVariable> cronVariables = Lists.newArrayList();
        final HashMap<Integer, Configuration> rawOverrideMap = Maps.newHashMap();
        final HashSet<CrontabExpression> results = Sets.newHashSet();

        int bad = 0;

        this.crontabTimestamp = crontabFile.lastModified();

        try {
            int lineNumber = 0;

            final ImmutableList<String> lines = Files.asCharSource(crontabFile, Charset.defaultCharset())
                    .readLines();

            ImmutableMap<ConfigKey, String> overrideMap = null;

            for (final String line : lines) {
                lineNumber++;

                final String trimmed = line.trim();

                if (trimmed.isEmpty()) {
                    continue;
                }

                if (line.startsWith(OVERRIDE_KEYWORD)) {

                    overrideMap = getOverrideConfiguration(line);

                    continue;
                }

                // If it's a variable assignment, save it in the map
                // and skip to the next row
                final CronVariable cronVariable = getVariable(trimmed);

                if (cronVariable != null) {
                    info("[Line: {0}] Found variable definition: {1} -> {2}", String.valueOf(lineNumber),
                            cronVariable.getName(), cronVariable.getValue());
                    cronVariables.add(cronVariable);
                    continue;
                }

                try {

                    final CrontabExpression crontabExpression = new CrontabExpression(lineNumber, trimmed);

                    if (crontabExpression.isCommented() && crontabExpression.isMalformed()) {
                        info("[Line: {0}] Skipping general comment: {1}", String.valueOf(lineNumber), line);
                        continue;
                    }

                    // crontabExpression.isCommented() || crontabExpression.isMalformed() || normal expression
                    // Commented rows that successfully parse as expressions are loaded anyways, to
                    // allow for alerting of "forgotten" disabled tasks
                    // Likewise, uncommented but malformed rows are also loaded so that malformed
                    // alerting can be done on them

                    results.add(crontabExpression);

                    // The previous non-blank/commented line is an unassociated override map. Associate with this row
                    if (overrideMap != null) {

                        info("[Line: {0}] Adding schedule with config overrides \"{1}\": {2}",
                                String.valueOf(lineNumber), getOverrideMapString(overrideMap),
                                crontabExpression.getCommand());

                        rawOverrideMap.put(lineNumber, configuration.withOverrides(overrideMap));

                        overrideMap = null;
                    } else {
                        info("[Line: {0}] Adding schedule: {1}", String.valueOf(lineNumber),
                                crontabExpression.getCommand());
                    }

                } catch (Exception e) {
                    bad++;
                    error("[Line: {0}] Failed to read crontab entry: {1}\n{2}", String.valueOf(lineNumber), trimmed,
                            Throwables.getStackTraceAsString(e));
                }

            }

        } catch (IOException e) {
            throw Throwables.propagate(e);
        }

        this.badRowCount = bad;
        this.variableList = ImmutableList.copyOf(cronVariables);
        this.crontabExpressions = ImmutableSet.copyOf(results);
        this.configurationOverrides = ImmutableMap.copyOf(rawOverrideMap);
    }

    private ImmutableMap<ConfigKey, String> getOverrideConfiguration(final String line) {

        // Override configuration line ->
        // #override: <ConfigKey>=value,...
        final HashMap<ConfigKey, String> result = Maps.newHashMap();

        final String noPrefix = line.substring(OVERRIDE_KEYWORD.length()).trim();

        final List<String> overrideList = Utils.COMMA_SPLITTER.splitToList(noPrefix);

        for (final String override : overrideList) {
            List<String> overrideParts = Utils.EQUAL_SPLITTER.splitToList(override);

            if (overrideParts.size() != 2) {
                warn("Malformed override: {0}", line);
                continue;
            }

            final ConfigKey configKey = ConfigKey.fromString(overrideParts.get(0));

            if (configKey == ConfigKey.Unknown) {
                warn("Malformed override: {0}", line);
                continue;
            }

            if (!configKey.allowOverride()) {
                warn("Cannot override {0}: {1}", configKey.getRawName(), line);
                continue;
            }

            result.put(configKey, overrideParts.get(1));

        }

        return ImmutableMap.copyOf(result);
    }

    private static CronVariable getVariable(final String line) {
        // Crontab variables like ->
        // <VARNAME>=value

        final int firstEqualIndex = line.indexOf('=');

        if (firstEqualIndex == -1) {
            return null;
        }

        final int firstQuoteIndex = line.indexOf('"');

        // varname is the entire value between the first non-whitespace char
        // and the first equal sign
        final String varName = line.substring(0, firstEqualIndex);

        // Variable names cannot contain whitespace
        if (CharMatcher.WHITESPACE.matchesAnyOf(varName)) {
            return null;
        }

        String varValue;

        // var values can be quoted strings
        // in such a case, the var value is everything between the first quote
        // and the last
        if (firstQuoteIndex > -1) {

            // If first quote comes before first equal sign, cannot be a var assignment
            if (firstQuoteIndex < firstEqualIndex) {
                return null;
            }

            varValue = line.substring(firstQuoteIndex + 1, line.lastIndexOf('"'));
        } else {
            varValue = line.substring(firstEqualIndex + 1, line.length());
        }

        return new CronVariable(varName, varValue);
    }

    /**
     * @return A set of {@link com.zulily.omicron.crontab.CrontabExpression} objects read from the crontab
     */
    public ImmutableSet<CrontabExpression> getCrontabExpressions() {
        return crontabExpressions;
    }

    /**
     * @return the number of crontab schedule rows that were considered 'bad' and cannot be executed
     */
    public int getBadRowCount() {
        return badRowCount;
    }

    /**
     * @return The last modified timestamp of the crontab file
     */
    public long getCrontabTimestamp() {
        return crontabTimestamp;
    }

    /**
     * @return A map of the variables defined in the crontab
     */
    public ImmutableList<CronVariable> getVariables() {
        return variableList;
    }

    /**
     * @return A map of Configuration overrides by the line number they are associated with
     */
    public ImmutableMap<Integer, Configuration> getConfigurationOverrides() {
        return configurationOverrides;
    }

    private String getOverrideMapString(final ImmutableMap<ConfigKey, String> overrideMap) {
        return COMMA_JOINER.join(overrideMap.entrySet().stream()
                .map(entry -> entry.getKey().getRawName() + "->" + entry.getValue()).collect(Collectors.toList()));
    }
}