io.spikex.notifier.Notifier.java Source code

Java tutorial

Introduction

Here is the source code for io.spikex.notifier.Notifier.java

Source

/**
 *
 * Copyright (c) 2015 NG Modular Oy.
 *
 * 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 io.spikex.notifier;

import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import static io.spikex.core.helper.Events.EVENT_FIELD_DESTINATIONS;
import static io.spikex.core.helper.Events.EVENT_FIELD_ID;
import static io.spikex.core.helper.Events.EVENT_FIELD_MESSAGE;
import static io.spikex.core.helper.Events.EVENT_FIELD_PRIORITY;
import static io.spikex.core.helper.Events.EVENT_FIELD_TITLE;
import io.spikex.core.helper.Variables;
import io.spikex.core.util.CronEntry;
import static io.spikex.core.util.Files.Permission.OWNER_FULL_GROUP_EXEC;
import io.spikex.core.util.Version;
import io.spikex.core.util.XXHash32;
import io.spikex.core.util.resource.TextResource;
import io.spikex.notifier.NotifierConfig.DestinationDef;
import io.spikex.notifier.NotifierConfig.TemplateDef;
import io.spikex.notifier.internal.Rule;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.unbescape.html.HtmlEscape;
import org.unbescape.json.JsonEscape;
import org.unbescape.uri.UriEscape;
import org.unbescape.xml.XmlEscape;
import org.vertx.java.core.Handler;
import org.vertx.java.core.eventbus.EventBus;
import org.vertx.java.core.eventbus.Message;
import org.vertx.java.core.json.JsonArray;
import org.vertx.java.core.json.JsonObject;

/**
 * @author cli
 */
public final class Notifier implements Handler<Message<JsonObject>> {

    // Configuration file and variables
    private final NotifierConfig m_config;
    private final Variables m_variables;

    // Configuration path and process user
    private final Path m_confPath;
    private final String m_user;

    // Notification queue and map
    private final Queue<JsonObject> m_queue;
    private final ConcurrentMap<String, JsonObject> m_map;

    // Mustache templates
    private final Map<String, Mustache> m_templates;

    // Template functions
    private final Map<String, Object> m_functions;

    private static final String EVENT_FIELD_NOTIF_SUCCESS = "@success";
    private static final String EVENT_FIELD_NOTIF_WARNING = "@warning";
    private static final String EVENT_FIELD_NOTIF_DANGER = "@danger";

    private static final String TEMPLATE_DIR = "template";
    private static final int HASH_SALT = 0x23881;

    private final Logger m_logger = LoggerFactory.getLogger(Notifier.class);

    public Notifier(final Queue<JsonObject> queue, final ConcurrentMap<String, JsonObject> map,
            final NotifierConfig config, final Variables variables, final Path confPath, final String user) {

        m_queue = queue;
        m_map = map;
        m_config = config;
        m_variables = variables;
        m_confPath = confPath;
        m_user = user;
        m_templates = new HashMap();

        // Functions
        m_functions = new HashMap();
        m_functions.put("escape-json", new Function<String, String>() {

            @Override
            public String apply(String text) {
                return JsonEscape.escapeJson(text);
            }
        });
        m_functions.put("escape-html5", new Function<String, String>() {

            @Override
            public String apply(String text) {
                return HtmlEscape.escapeHtml5(text);
            }
        });
        m_functions.put("escape-html4", new Function<String, String>() {

            @Override
            public String apply(String text) {
                return HtmlEscape.escapeHtml4Xml(text);
            }
        });
        m_functions.put("escape-xml11", new Function<String, String>() {

            @Override
            public String apply(String text) {
                return XmlEscape.escapeXml11(text);
            }
        });
        m_functions.put("escape-xml10", new Function<String, String>() {

            @Override
            public String apply(String text) {
                return XmlEscape.escapeXml10Minimal(text);
            }
        });
        m_functions.put("escape-uri-path", new Function<String, String>() {

            @Override
            public String apply(String text) {
                return UriEscape.escapeUriPath(text);
            }
        });
    }

    public void start(final EventBus eventBus) {
        //
        // Build Mustasche templates
        //
        buildTemplates();
        //
        // Start listening for events
        //
        m_logger.info("Listening on local address: {}", m_config.getLocalAddress());
        eventBus.registerLocalHandler(m_config.getLocalAddress(), this);
    }

    public void stop(final EventBus eventBus) {
        eventBus.unregisterHandler(m_config.getLocalAddress(), this);
    }

    public void buildTemplates() {
        //
        // Build mustasche templates
        //
        m_templates.clear();
        Map<String, TemplateDef> templates = m_config.getTemplates();
        MustacheFactory mstFactory = new DefaultMustacheFactory();

        Set<String> keys = templates.keySet();
        for (String key : keys) {

            TemplateDef def = templates.get(key);
            List<String> locales = new ArrayList();
            locales.add(Locale.ROOT.toString()); // No locale
            locales.addAll(def.getLocales());

            for (String locale : locales) {
                Locale loc = new Locale(locale);
                String url = m_variables.translate(def.getUrl());
                String tmpl = key + "-" + loc.toString();
                if (Locale.ROOT.equals(loc)) {
                    tmpl = key;
                }
                m_logger.debug("Building Mustasche template: {} ({})", tmpl, url);
                m_templates.put(tmpl, buildTemplate(m_confPath, m_user, mstFactory, url, loc));
            }
        }
    }

    @Override
    public void handle(final Message<JsonObject> message) {
        //
        // Find matching rule
        //
        m_logger.debug("Received: {}", message.body());
        DateTime now = DateTime.now();
        String timezone = now.getZone().getID();
        Map<String, CronEntry> schedules = m_config.getSchedules();
        List<Rule> rules = m_config.getRules();
        for (Rule rule : rules) {
            //
            // Matching schedule and tags?
            //
            CronEntry entry = schedules.get(rule.getSchedule());
            if (!timezone.equals(entry.getTimezone())) {
                timezone = entry.getTimezone();
                now = DateTime.now(DateTimeZone.forID(timezone));
            }
            // We must make a copy (we want to replace @message with the translated template contents)
            JsonObject event = new JsonObject().mergeIn(message.body());
            if (entry.isDefined(now) && rule.match(event)) {

                List<String> destinations = rule.getDestinations();
                m_logger.debug("Rule \"{}\" matched - template: {} notifying: {}", rule.getName(),
                        rule.getTemplate(), destinations);

                //
                // Add optional fields (if missing) - so that templates work correctly
                //
                if (!event.containsField(EVENT_FIELD_NOTIF_SUCCESS)) {
                    event.putBoolean(EVENT_FIELD_NOTIF_SUCCESS, false);
                }
                if (!event.containsField(EVENT_FIELD_NOTIF_WARNING)) {
                    event.putBoolean(EVENT_FIELD_NOTIF_WARNING, false);
                }
                if (!event.containsField(EVENT_FIELD_NOTIF_DANGER)) {
                    event.putBoolean(EVENT_FIELD_NOTIF_DANGER, false);
                }

                //
                // Translate subject and fill/execute template
                //
                String subject = m_variables.translate(event, rule.getSubject());
                Mustache template = m_templates.get(rule.getTemplate());
                StringWriter writer = new StringWriter();
                Map scopes = new HashMap(event.toMap());
                scopes.putAll(m_functions); // Functions
                template.execute(writer, scopes);
                List<String> resolvedDestinations = resolveDestinations(destinations);

                //
                // Fill event with notification info
                //
                event.putString(EVENT_FIELD_TITLE, subject);
                event.putString(EVENT_FIELD_MESSAGE, writer.toString());
                event.putArray(EVENT_FIELD_DESTINATIONS, new JsonArray(resolvedDestinations));

                if (m_map != null && m_queue != null) {

                    // Calculate event hash
                    String hash = XXHash32.hashAsHex(event.toString(), HASH_SALT);

                    m_logger.info("Storing notification: {} priority: {} destinations: {} subject: {} hash: {}",
                            event.getString(EVENT_FIELD_ID, ""), event.getString(EVENT_FIELD_PRIORITY, ""),
                            resolvedDestinations, subject, hash);

                    if (m_map.putIfAbsent(hash, event) == null) {
                        m_queue.add(event);
                    } else {
                        m_logger.info("Ignoring duplicate event: {}", hash);
                    }

                } else {
                    // Log event
                    m_logger.error("Notification map has not been initialized. " + "Could not handle event: {}",
                            event.toString());
                }
            }
        }
    }

    private Mustache buildTemplate(final Path confPath, final String user, final MustacheFactory factory,
            final String url, final Locale locale) {

        Mustache mst = null;
        try {
            //
            // Check if template exists
            //
            Path urlPath = Paths.get(url);
            Path basePath = confPath.resolve(TEMPLATE_DIR);
            String name = resolveResourceName(urlPath);
            String suffix = resolveResourceSuffix(urlPath);
            TextResource template = TextResource.builder(basePath.toUri()).name(name).suffix(suffix)
                    .location(urlPath.getFileName().toUri()).locale(locale)
                    .version(Version.create(name, Version.nullVersion())).build();

            // Create template dir if it doesn't exist
            if (!Files.exists(basePath)) {
                io.spikex.core.util.Files.createDirectories(user, OWNER_FULL_GROUP_EXEC, basePath);
            }

            if (!template.exists()) {
                //
                // Load and save template
                //
                m_logger.info("Saving \"{}\" template in: {}", template.getQualifiedName(), basePath);
                TextResource origTemplate = TextResource.builder(urlPath.getParent().toUri(), template).build()
                        .load();

                template = TextResource.builder(basePath.toUri(), origTemplate).build().save();
            } else {
                m_logger.info("Loading template: {}", template.getQualifiedName());
                template = template.load();
            }

            //
            // Compile template
            //
            mst = factory.compile(new StringReader(template.getData()), name);

        } catch (IOException e) {
            throw new IllegalStateException("Failed to build template: " + url, e);
        }
        return mst;
    }

    private List<String> resolveDestinations(final List<String> destinations) {
        List<String> resolved = new ArrayList();
        Map<String, DestinationDef> defs = m_config.getDestinations();
        for (String destination : destinations) {
            DestinationDef def = defs.get(destination);
            List<String> dests = def.geAddresses();
            resolved.addAll(dests);
        }
        return resolved;
    }

    private String resolveResourceName(final Path filename) {
        String name = filename.getFileName().toString();
        int pos = name.lastIndexOf('.');
        if (pos > 0) {
            name = name.substring(0, pos);
        }
        return name;
    }

    private String resolveResourceSuffix(final Path filename) {
        String suffix = "";
        String name = filename.getFileName().toString();
        int pos = name.lastIndexOf('.');
        if (pos > 0) {
            suffix = name.substring(pos);
        }
        return suffix;
    }
}