com.google.gerrit.server.change.ReviewerSuggestionCache.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.server.change.ReviewerSuggestionCache.java

Source

// Copyright (C) 2014 The Android Open Source Project
//
// 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.google.gerrit.server.change;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.base.Splitter;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.AccountExternalIdAccess;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.analysis.util.CharArraySet;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.IntField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.RAMDirectory;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * The suggest oracle may be called many times in rapid succession during the
 * course of one operation.
 * It would be easy to have a simple {@code Cache<Boolean, List<Account>>}
 * with a short expiration time of 30s.
 * Cache only has a single key we're just using Cache for the expiration behavior.
 */
@Singleton
public class ReviewerSuggestionCache {
    private static final Logger log = LoggerFactory.getLogger(ReviewerSuggestionCache.class);

    private static final String ID = "id";
    private static final String NAME = "name";
    private static final String EMAIL = "email";
    private static final String USERNAME = "username";
    private static final String[] ALL = { ID, NAME, EMAIL, USERNAME };

    private final LoadingCache<Boolean, IndexSearcher> cache;
    private final Provider<ReviewDb> db;

    @Inject
    ReviewerSuggestionCache(Provider<ReviewDb> db, @GerritServerConfig Config cfg) {
        this.db = db;
        long expiration = ConfigUtil.getTimeUnit(cfg, "suggest", null, "fullTextSearchRefresh",
                TimeUnit.HOURS.toMillis(1), TimeUnit.MILLISECONDS);
        this.cache = CacheBuilder.newBuilder().maximumSize(1).refreshAfterWrite(expiration, TimeUnit.MILLISECONDS)
                .build(new CacheLoader<Boolean, IndexSearcher>() {
                    @Override
                    public IndexSearcher load(Boolean key) throws Exception {
                        return index();
                    }
                });
    }

    List<AccountInfo> search(String query, int n) throws IOException {
        IndexSearcher searcher = get();
        if (searcher == null) {
            return Collections.emptyList();
        }

        List<String> segments = Splitter.on(' ').omitEmptyStrings().splitToList(query.toLowerCase());
        BooleanQuery q = new BooleanQuery();
        for (String field : ALL) {
            BooleanQuery and = new BooleanQuery();
            for (String s : segments) {
                and.add(new PrefixQuery(new Term(field, s)), Occur.MUST);
            }
            q.add(and, Occur.SHOULD);
        }

        TopDocs results = searcher.search(q, n);
        ScoreDoc[] hits = results.scoreDocs;

        List<AccountInfo> result = new LinkedList<>();

        for (ScoreDoc h : hits) {
            Document doc = searcher.doc(h.doc);

            IndexableField idField = checkNotNull(doc.getField(ID));
            AccountInfo info = new AccountInfo(idField.numericValue().intValue());
            info.name = doc.get(NAME);
            info.email = doc.get(EMAIL);
            info.username = doc.get(USERNAME);
            result.add(info);
        }

        return result;
    }

    private IndexSearcher get() {
        try {
            return cache.get(true);
        } catch (ExecutionException e) {
            log.warn("Cannot fetch reviewers from cache", e);
            return null;
        }
    }

    private IndexSearcher index() throws IOException, OrmException {
        RAMDirectory idx = new RAMDirectory();
        IndexWriterConfig config = new IndexWriterConfig(new StandardAnalyzer(CharArraySet.EMPTY_SET));
        config.setOpenMode(OpenMode.CREATE);

        try (IndexWriter writer = new IndexWriter(idx, config)) {
            for (Account a : db.get().accounts().all()) {
                if (a.isActive()) {
                    addAccount(writer, a);
                }
            }
        }

        return new IndexSearcher(DirectoryReader.open(idx));
    }

    private void addAccount(IndexWriter writer, Account a) throws IOException, OrmException {
        Document doc = new Document();
        doc.add(new IntField(ID, a.getId().get(), Store.YES));
        if (a.getFullName() != null) {
            doc.add(new TextField(NAME, a.getFullName(), Store.YES));
        }
        if (a.getPreferredEmail() != null) {
            doc.add(new StringField(EMAIL, a.getPreferredEmail().toLowerCase(), Store.YES));
            doc.add(new TextField(EMAIL, a.getPreferredEmail(), Store.YES));
        }
        AccountExternalIdAccess extIdAccess = db.get().accountExternalIds();
        String username = AccountState.getUserName(extIdAccess.byAccount(a.getId()).toList());
        if (username != null) {
            doc.add(new StringField(USERNAME, username, Store.YES));
        }
        writer.addDocument(doc);
    }
}