com.ruesga.gerrit.plugins.fcm.handlers.EventHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.ruesga.gerrit.plugins.fcm.handlers.EventHandler.java

Source

/*
 * Copyright (C) 2016 Jorge Ruesga
 *
 * 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.ruesga.gerrit.plugins.fcm.handlers;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.events.ChangeEvent;
import com.google.gerrit.extensions.events.RevisionEvent;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.index.query.QueryResult;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.IdentifiedUser.GenericFactory;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.ProjectWatches.NotifyType;
import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.NotifyConfig;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeQueryProcessor;
import com.google.gerrit.server.query.change.SingleGroupUser;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
import com.ruesga.gerrit.plugins.fcm.messaging.Notification;
import com.ruesga.gerrit.plugins.fcm.workers.FcmUploaderWorker;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class EventHandler {

    private static final Logger log = LoggerFactory.getLogger(EventHandler.class);

    private final String pluginName;
    private final FcmUploaderWorker uploader;
    private final AllProjectsName allProjectsName;
    private final ChangeQueryBuilder cqb;
    private final ChangeQueryProcessor cqp;
    private final ProjectCache projectCache;
    private final GroupBackend groupBackend;
    private final Provider<InternalAccountQuery> accountQueryProvider;
    private final GenericFactory identifiedUserFactory;
    private final Provider<AnonymousUser> anonymousProvider;
    private final Gson gson;

    public EventHandler(@PluginName String pluginName, FcmUploaderWorker uploader, AllProjectsName allProjectsName,
            ChangeQueryBuilder cqb, ChangeQueryProcessor cqp, ProjectCache projectCache, GroupBackend groupBackend,
            Provider<InternalAccountQuery> accountQueryProvider, GenericFactory identifiedUserFactory,
            Provider<AnonymousUser> anonymousProvider) {
        super();
        this.pluginName = pluginName;
        this.uploader = uploader;
        this.allProjectsName = allProjectsName;
        this.cqb = cqb;
        this.cqp = cqp;
        this.projectCache = projectCache;
        this.groupBackend = groupBackend;
        this.accountQueryProvider = accountQueryProvider;
        this.identifiedUserFactory = identifiedUserFactory;
        this.anonymousProvider = anonymousProvider;
        this.gson = new GsonBuilder().create();
    }

    protected abstract int getEventType();

    protected abstract NotifyType getNotifyType();

    Gson getSerializer() {
        return this.gson;
    }

    protected Notification createNotification(ChangeEvent event) {
        Notification notification = new Notification();
        notification.event = getEventType();
        notification.when = event.getWhen().getTime() / 1000L;
        notification.who = event.getWho();
        notification.change = event.getChange().changeId;
        notification.legacyChangeId = event.getChange()._number;
        notification.project = event.getChange().project;
        notification.branch = event.getChange().branch;
        notification.topic = event.getChange().topic;
        notification.subject = StringUtils.abbreviate(event.getChange().subject, 100);
        if (event instanceof RevisionEvent) {
            notification.revision = ((RevisionEvent) event).getRevision().commit.commit;
        }
        return notification;
    }

    protected void notify(Notification notification, ChangeEvent event) {
        // Check if this event should be notified
        if (event.getNotify().equals(NotifyHandling.NONE)) {
            if (log.isDebugEnabled()) {
                log.debug(String.format("[%s] Notify event %d is not enabled: %s", pluginName, getEventType(),
                        gson.toJson(notification)));
            }
            return;
        }

        // Obtain information about the accounts that need to be
        // notified related to this event
        List<Integer> notifiedUsers = obtainNotifiedAccounts(event);
        if (notifiedUsers.isEmpty()) {
            // Nobody to notify about this event
            return;
        }

        // Perform notification
        if (log.isDebugEnabled()) {
            log.debug(String.format("[%s] Sending notification %s to %s", pluginName, gson.toJson(notification),
                    gson.toJson(notifiedUsers)));
        }
        this.uploader.notifyTo(notifiedUsers, notification);
    }

    private List<Integer> obtainNotifiedAccounts(ChangeEvent event) {
        Set<Integer> notifiedUsers = new HashSet<>();
        ChangeInfo change = event.getChange();
        NotifyHandling notifyTo = event.getNotify();

        // 1.- Owner of the change
        notifiedUsers.add(change.owner._accountId);

        // 2.- Reviewers
        if (notifyTo.equals(NotifyHandling.OWNER_REVIEWERS) || notifyTo.equals(NotifyHandling.ALL)) {
            if (change.reviewers != null) {
                for (ReviewerState state : change.reviewers.keySet()) {
                    Collection<AccountInfo> accounts = change.reviewers.get(state);
                    for (AccountInfo account : accounts) {
                        notifiedUsers.add(account._accountId);
                    }
                }
            }
        }

        // 3.- Watchers
        ChangeData changeData = obtainChangeData(change);
        if (changeData != null) {
            notifiedUsers.addAll(getWatchers(getNotifyType(), changeData,
                    !safeBoolean(change.workInProgress) && !safeBoolean(change.isPrivate)));
        }

        // 4.- Remove the author of this event (he doesn't need to get
        // the notification)
        notifiedUsers.remove(event.getWho()._accountId);

        return new ArrayList<>(notifiedUsers);
    }

    private Set<Integer> getWatchers(NotifyType type, ChangeData change, boolean includeWatchersFromNotifyConfig) {
        Set<Integer> watchers = new HashSet<>();
        try {
            Set<Account.Id> projectWatchers = new HashSet<>();
            for (AccountState a : accountQueryProvider.get().byWatchedProject(change.project())) {
                Account.Id accountId = a.getAccount().getId();
                for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyType>> e : a.getProjectWatches().entrySet()) {
                    if (change.project().equals(e.getKey().project())
                            && add(watchers, accountId, e.getKey(), e.getValue(), type, change)) {
                        // We only want to prevent matching All-Projects if this filter hits
                        projectWatchers.add(accountId);
                    }
                }
            }

            for (AccountState a : accountQueryProvider.get().byWatchedProject(allProjectsName)) {
                for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyType>> e : a.getProjectWatches().entrySet()) {
                    if (allProjectsName.equals(e.getKey().project())) {
                        Account.Id accountId = a.getAccount().getId();
                        if (!projectWatchers.contains(accountId)) {
                            add(watchers, accountId, e.getKey(), e.getValue(), type, change);
                        }
                    }
                }
            }

            if (includeWatchersFromNotifyConfig) {
                ProjectState projectState = projectCache.get(change.project());
                if (projectState != null) {
                    for (ProjectState state : projectState.tree()) {
                        for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) {
                            if (nc.isNotify(type)) {
                                try {
                                    add(watchers, nc, change);
                                } catch (QueryParseException e) {
                                    log.warn("Project {} has invalid notify {} filter \"{}\": {}", state.getName(),
                                            nc.getName(), nc.getFilter(), e.getMessage());
                                }
                            }
                        }
                    }
                }
            }

        } catch (OrmException ex) {
            log.error(String.format("[%s] Failed to obtain watchers", pluginName), ex);
        }
        return watchers;
    }

    private boolean add(Set<Integer> watchers, Account.Id accountId, ProjectWatchKey key,
            Set<NotifyType> watchedTypes, NotifyType type, ChangeData change) throws OrmException {
        IdentifiedUser user = identifiedUserFactory.create(accountId);

        try {
            if (filterMatch(user, key.filter(), change)) {
                // If we are set to notify on this type, add the user.
                // Otherwise, still return true to stop notifications for this user.
                if (watchedTypes.contains(type)) {
                    watchers.add(accountId.get());
                }
                return true;
            }
        } catch (QueryParseException e) {
            // Ignore broken filter expressions.
        }
        return false;
    }

    private void add(Set<Integer> watchers, NotifyConfig nc, ChangeData change)
            throws OrmException, QueryParseException {
        for (GroupReference ref : nc.getGroups()) {
            CurrentUser user = new SingleGroupUser(ref.getUUID());
            if (filterMatch(user, nc.getFilter(), change)) {
                deliverToMembers(watchers, ref.getUUID());
            }
        }
    }

    private boolean filterMatch(CurrentUser user, String filter, ChangeData change)
            throws OrmException, QueryParseException {
        ChangeQueryBuilder qb;
        Predicate<ChangeData> p = null;
        if (user == null) {
            qb = cqb.asUser(anonymousProvider.get());
        } else {
            qb = cqb.asUser(user);
            p = qb.is_visible();
        }

        if (filter != null) {
            Predicate<ChangeData> filterPredicate = qb.parse(filter);
            if (p == null) {
                p = filterPredicate;
            } else {
                p = Predicate.and(filterPredicate, p);
            }
        }
        return p == null || p.asMatchable().match(change);
    }

    private ChangeData obtainChangeData(ChangeInfo change) {
        try {
            QueryResult<ChangeData> changeQuery = cqp.query(cqb.parse("change:" + change._number));
            List<ChangeData> changeQueryResults = changeQuery.entities();
            if (changeQueryResults == null || changeQueryResults.isEmpty()) {
                log.warn(String.format("[%s] No change found for %s", pluginName, change._number));
                return null;
            }
            return changeQueryResults.get(0);

        } catch (Exception ex) {
            log.error(String.format("[%s] Failed to obtain change data: %d", pluginName, change._number), ex);
        }
        return null;
    }

    private void deliverToMembers(Set<Integer> watchers, AccountGroup.UUID startUUID) {
        Set<AccountGroup.UUID> seen = new HashSet<>();
        List<AccountGroup.UUID> q = new ArrayList<>();

        seen.add(startUUID);
        q.add(startUUID);

        while (!q.isEmpty()) {
            AccountGroup.UUID uuid = q.remove(q.size() - 1);
            GroupDescription.Basic group = groupBackend.get(uuid);
            if (group == null) {
                continue;
            }

            if (!(group instanceof GroupDescription.Internal)) {
                // Non-internal groups cannot be expanded by the server.
                continue;
            }

            GroupDescription.Internal ig = (GroupDescription.Internal) group;
            for (Account.Id id : ig.getMembers()) {
                watchers.add(id.get());
            }
            for (AccountGroup.UUID m : ig.getSubgroups()) {
                if (seen.add(m)) {
                    q.add(m);
                }
            }
        }
    }

    protected String formatAccount(AccountInfo account) {
        if (account.name != null) {
            return account.name;
        }
        if (account.username != null) {
            return account.username;
        }
        return account.email;
    }

    boolean safeBoolean(Boolean value) {
        return value != null && value;
    }
}