// Copyright (C) 2010 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.events; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelTypes; import com.google.gerrit.common.data.SubmitRecord; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.ChangeMessage; import com.google.gerrit.reviewdb.client.Patch; import com.google.gerrit.reviewdb.client.PatchLineComment; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSetAncestor; import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.client.RevId; import com.google.gerrit.reviewdb.client.UserIdentity; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ApprovalsUtil; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.change.ChangeKindCache; import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.data.AccountAttribute; import com.google.gerrit.server.data.ApprovalAttribute; import com.google.gerrit.server.data.ChangeAttribute; import com.google.gerrit.server.data.DependencyAttribute; import com.google.gerrit.server.data.MessageAttribute; import com.google.gerrit.server.data.PatchAttribute; import com.google.gerrit.server.data.PatchSetAttribute; import com.google.gerrit.server.data.PatchSetCommentAttribute; import com.google.gerrit.server.data.RefUpdateAttribute; import com.google.gerrit.server.data.SubmitLabelAttribute; import com.google.gerrit.server.data.SubmitRecordAttribute; import com.google.gerrit.server.data.TrackingIdAttribute; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.patch.PatchList; import com.google.gerrit.server.patch.PatchListCache; import com.google.gerrit.server.patch.PatchListEntry; import com.google.gerrit.server.patch.PatchListNotAvailableException; import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; import com.google.gerrit.server.query.change.ChangeData; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @Singleton public class EventFactory { private static final Logger log = LoggerFactory.getLogger(EventFactory.class); private final AccountCache accountCache; private final Provider<String> urlProvider; private final PatchListCache patchListCache; private final SchemaFactory<ReviewDb> schema; private final PatchSetInfoFactory psInfoFactory; private final PersonIdent myIdent; private final Provider<ReviewDb> db; private final ChangeData.Factory changeDataFactory; private final ApprovalsUtil approvalsUtil; private final ChangeKindCache changeKindCache; @Inject EventFactory(AccountCache accountCache, @CanonicalWebUrl @Nullable Provider<String> urlProvider, PatchSetInfoFactory psif, PatchListCache patchListCache, SchemaFactory<ReviewDb> schema, @GerritPersonIdent PersonIdent myIdent, Provider<ReviewDb> db, ChangeData.Factory changeDataFactory, ApprovalsUtil approvalsUtil, ChangeKindCache changeKindCache) { this.accountCache = accountCache; this.urlProvider = urlProvider; this.patchListCache = patchListCache; this.schema = schema; this.psInfoFactory = psif; this.myIdent = myIdent; this.db = db; this.changeDataFactory = changeDataFactory; this.approvalsUtil = approvalsUtil; this.changeKindCache = changeKindCache; } /** * Create a ChangeAttribute for the given change suitable for serialization to * JSON. * * @param change * @return object suitable for serialization to JSON */ public ChangeAttribute asChangeAttribute(final Change change) { ChangeAttribute a = new ChangeAttribute(); a.project = change.getProject().get(); a.branch = change.getDest().getShortName(); a.topic = change.getTopic(); a.id = change.getKey().get(); a.number = change.getId().toString(); a.subject = change.getSubject(); try { a.commitMessage = changeDataFactory.create(db.get(), change).commitMessage(); } catch (Exception e) { log.error("Error while getting full commit message for" + " change " + a.number); } a.url = getChangeUrl(change); a.owner = asAccountAttribute(change.getOwner()); a.status = change.getStatus(); return a; } /** * Create a RefUpdateAttribute for the given old ObjectId, new ObjectId, and * branch that is suitable for serialization to JSON. * * @param oldId * @param newId * @param refName * @return object suitable for serialization to JSON */ public RefUpdateAttribute asRefUpdateAttribute(final ObjectId oldId, final ObjectId newId, final Branch.NameKey refName) { RefUpdateAttribute ru = new RefUpdateAttribute(); ru.newRev = newId != null ? newId.getName() : ObjectId.zeroId().getName(); ru.oldRev = oldId != null ? oldId.getName() : ObjectId.zeroId().getName(); ru.project = refName.getParentKey().get(); ru.refName = refName.getShortName(); return ru; } /** * Extend the existing ChangeAttribute with additional fields. * * @param a * @param change */ public void extend(ChangeAttribute a, Change change) { a.createdOn = change.getCreatedOn().getTime() / 1000L; a.lastUpdated = change.getLastUpdatedOn().getTime() / 1000L; a.sortKey = change.getSortKey(); a.open = change.getStatus().isOpen(); } /** * Add allReviewers to an existing ChangeAttribute. * * @param a * @param notes */ public void addAllReviewers(ChangeAttribute a, ChangeNotes notes) throws OrmException { Collection<Account.Id> reviewers = approvalsUtil.getReviewers(db.get(), notes).values(); if (!reviewers.isEmpty()) { a.allReviewers = Lists.newArrayListWithCapacity(reviewers.size()); for (Account.Id id : reviewers) { a.allReviewers.add(asAccountAttribute(id)); } } } /** * Add submitRecords to an existing ChangeAttribute. * * @param ca * @param submitRecords */ public void addSubmitRecords(ChangeAttribute ca, List<SubmitRecord> submitRecords) { ca.submitRecords = new ArrayList<>(); for (SubmitRecord submitRecord : submitRecords) { SubmitRecordAttribute sa = new SubmitRecordAttribute(); sa.status = submitRecord.status.name(); if (submitRecord.status != SubmitRecord.Status.RULE_ERROR) { addSubmitRecordLabels(submitRecord, sa); } ca.submitRecords.add(sa); } // Remove empty lists so a confusing label won't be displayed in the output. if (ca.submitRecords.isEmpty()) { ca.submitRecords = null; } } private void addSubmitRecordLabels(SubmitRecord submitRecord, SubmitRecordAttribute sa) { if (submitRecord.labels != null && !submitRecord.labels.isEmpty()) { sa.labels = new ArrayList<>(); for (SubmitRecord.Label lbl : submitRecord.labels) { SubmitLabelAttribute la = new SubmitLabelAttribute(); la.label = lbl.label; la.status = lbl.status.name(); if(lbl.appliedBy != null) { Account a = accountCache.get(lbl.appliedBy).getAccount(); la.by = asAccountAttribute(a); } sa.labels.add(la); } } } public void addDependencies(ChangeAttribute ca, Change change) { ca.dependsOn = new ArrayList<>(); ca.neededBy = new ArrayList<>(); try { final ReviewDb db = schema.open(); try { final PatchSet.Id psId = change.currentPatchSetId(); for (PatchSetAncestor a : db.patchSetAncestors().ancestorsOf(psId)) { for (PatchSet p : db.patchSets().byRevision(a.getAncestorRevision())) { Change c = db.changes().get(p.getId().getParentKey()); ca.dependsOn.add(newDependsOn(c, p)); } } final PatchSet ps = db.patchSets().get(psId); if (ps == null) { log.error("Error while generating the list of descendants for" + " PatchSet " + psId + ": Cannot find PatchSet entry in" + " database."); } else { final RevId revId = ps.getRevision(); for (PatchSetAncestor a : db.patchSetAncestors().descendantsOf(revId)) { final PatchSet p = db.patchSets().get(a.getPatchSet()); if (p == null) { log.error("Error while generating the list of descendants for" + " revision " + revId.get() + ": Cannot find PatchSet entry in" + " database for " + a.getPatchSet()); continue; } final Change c = db.changes().get(p.getId().getParentKey()); ca.neededBy.add(newNeededBy(c, p)); } } } finally { db.close(); } } catch (OrmException e) { // Squash DB exceptions and leave dependency lists partially filled. } // Remove empty lists so a confusing label won't be displayed in the output. if (ca.dependsOn.isEmpty()) { ca.dependsOn = null; } if (ca.neededBy.isEmpty()) { ca.neededBy = null; } } private DependencyAttribute newDependsOn(Change c, PatchSet ps) { DependencyAttribute d = newDependencyAttribute(c, ps); d.isCurrentPatchSet = ps.getId().equals(c.currentPatchSetId()); return d; } private DependencyAttribute newNeededBy(Change c, PatchSet ps) { return newDependencyAttribute(c, ps); } private DependencyAttribute newDependencyAttribute(Change c, PatchSet ps) { DependencyAttribute d = new DependencyAttribute(); d.number = c.getId().toString(); d.id = c.getKey().toString(); d.revision = ps.getRevision().get(); d.ref = ps.getRefName(); return d; } public void addTrackingIds(ChangeAttribute a, Multimap<String, String> set) { if (!set.isEmpty()) { a.trackingIds = new ArrayList<>(set.size()); for (Map.Entry<String, Collection<String>> e : set.asMap().entrySet()) { for (String id : e.getValue()) { TrackingIdAttribute t = new TrackingIdAttribute(); t.system = e.getKey(); t.id = id; a.trackingIds.add(t); } } } } public void addCommitMessage(ChangeAttribute a, String commitMessage) { a.commitMessage = commitMessage; } public void addPatchSets(ChangeAttribute a, Collection<PatchSet> ps, LabelTypes labelTypes) { addPatchSets(a, ps, null, false, null, labelTypes); } public void addPatchSets(ChangeAttribute ca, Collection<PatchSet> ps, Map<PatchSet.Id, Collection<PatchSetApproval>> approvals, LabelTypes labelTypes) { addPatchSets(ca, ps, approvals, false, null, labelTypes); } public void addPatchSets(ChangeAttribute ca, Collection<PatchSet> ps, Map<PatchSet.Id, Collection<PatchSetApproval>> approvals, boolean includeFiles, Change change, LabelTypes labelTypes) { if (!ps.isEmpty()) { ca.patchSets = new ArrayList<>(ps.size()); for (PatchSet p : ps) { PatchSetAttribute psa = asPatchSetAttribute(p); if (approvals != null) { addApprovals(psa, p.getId(), approvals, labelTypes); } ca.patchSets.add(psa); if (includeFiles && change != null) { addPatchSetFileNames(psa, change, p); } } } } public void addPatchSetComments(PatchSetAttribute patchSetAttribute, Collection<PatchLineComment> patchLineComments) { for (PatchLineComment comment : patchLineComments) { if (comment.getKey().getParentKey().getParentKey().get() == Integer.parseInt(patchSetAttribute.number)) { if (patchSetAttribute.comments == null) { patchSetAttribute.comments = new ArrayList<>(); } patchSetAttribute.comments.add(asPatchSetLineAttribute(comment)); } } } public void addPatchSetFileNames(PatchSetAttribute patchSetAttribute, Change change, PatchSet patchSet) { try { PatchList patchList = patchListCache.get(change, patchSet); for (PatchListEntry patch : patchList.getPatches()) { if (patchSetAttribute.files == null) { patchSetAttribute.files = new ArrayList<>(); } PatchAttribute p = new PatchAttribute(); p.file = patch.getNewName(); p.fileOld = patch.getOldName(); p.type = patch.getChangeType(); p.deletions -= patch.getDeletions(); p.insertions = patch.getInsertions(); patchSetAttribute.files.add(p); } } catch (PatchListNotAvailableException e) { } } public void addComments(ChangeAttribute ca, Collection<ChangeMessage> messages) { if (!messages.isEmpty()) { ca.comments = new ArrayList<>(); for (ChangeMessage message : messages) { ca.comments.add(asMessageAttribute(message)); } } } /** * Create a PatchSetAttribute for the given patchset suitable for * serialization to JSON. * * @param patchSet * @return object suitable for serialization to JSON */ public PatchSetAttribute asPatchSetAttribute(final PatchSet patchSet) { PatchSetAttribute p = new PatchSetAttribute(); p.revision = patchSet.getRevision().get(); p.number = Integer.toString(patchSet.getPatchSetId()); p.ref = patchSet.getRefName(); p.uploader = asAccountAttribute(patchSet.getUploader()); p.createdOn = patchSet.getCreatedOn().getTime() / 1000L; p.isDraft = patchSet.isDraft(); final PatchSet.Id pId = patchSet.getId(); try { final ReviewDb db = schema.open(); try { p.parents = new ArrayList<>(); for (PatchSetAncestor a : db.patchSetAncestors().ancestorsOf( patchSet.getId())) { p.parents.add(a.getAncestorRevision().get()); } UserIdentity author = psInfoFactory.get(db, pId).getAuthor(); if (author.getAccount() == null) { p.author = new AccountAttribute(); p.author.email = author.getEmail(); p.author.name = author.getName(); p.author.username = ""; } else { p.author = asAccountAttribute(author.getAccount()); } Change change = db.changes().get(pId.getParentKey()); List<Patch> list = patchListCache.get(change, patchSet).toPatchList(pId); for (Patch pe : list) { if (!Patch.COMMIT_MSG.equals(pe.getFileName())) { p.sizeDeletions -= pe.getDeletions(); p.sizeInsertions += pe.getInsertions(); } } p.kind = changeKindCache.getChangeKind(db, change, patchSet); } finally { db.close(); } } catch (OrmException e) { log.error("Cannot load patch set data for " + patchSet.getId(), e); } catch (PatchSetInfoNotAvailableException e) { log.error(String.format("Cannot get authorEmail for %s.", pId), e); } catch (PatchListNotAvailableException e) { log.error(String.format("Cannot get size information for %s.", pId), e); } return p; } public void addApprovals(PatchSetAttribute p, PatchSet.Id id, Map<PatchSet.Id, Collection<PatchSetApproval>> all, LabelTypes labelTypes) { Collection<PatchSetApproval> list = all.get(id); if (list != null) { addApprovals(p, list, labelTypes); } } public void addApprovals(PatchSetAttribute p, Collection<PatchSetApproval> list, LabelTypes labelTypes) { if (!list.isEmpty()) { p.approvals = new ArrayList<>(list.size()); for (PatchSetApproval a : list) { if (a.getValue() != 0) { p.approvals.add(asApprovalAttribute(a, labelTypes)); } } if (p.approvals.isEmpty()) { p.approvals = null; } } } /** * Create an AuthorAttribute for the given account suitable for serialization * to JSON. * * @param id * @return object suitable for serialization to JSON */ public AccountAttribute asAccountAttribute(Account.Id id) { if (id == null) { return null; } return asAccountAttribute(accountCache.get(id).getAccount()); } /** * Create an AuthorAttribute for the given account suitable for serialization * to JSON. * * @param account * @return object suitable for serialization to JSON */ public AccountAttribute asAccountAttribute(final Account account) { AccountAttribute who = new AccountAttribute(); who.name = account.getFullName(); who.email = account.getPreferredEmail(); who.username = account.getUserName(); return who; } /** * Create an AuthorAttribute for the given person ident suitable for * serialization to JSON. * * @param ident * @return object suitable for serialization to JSON */ public AccountAttribute asAccountAttribute(PersonIdent ident) { AccountAttribute who = new AccountAttribute(); who.name = ident.getName(); who.email = ident.getEmailAddress(); return who; } /** * Create an ApprovalAttribute for the given approval suitable for * serialization to JSON. * * @param approval * @param labelTypes label types for the containing project * @return object suitable for serialization to JSON */ public ApprovalAttribute asApprovalAttribute(PatchSetApproval approval, LabelTypes labelTypes) { ApprovalAttribute a = new ApprovalAttribute(); a.type = approval.getLabelId().get(); a.value = Short.toString(approval.getValue()); a.by = asAccountAttribute(approval.getAccountId()); a.grantedOn = approval.getGranted().getTime() / 1000L; LabelType lt = labelTypes.byLabel(approval.getLabelId()); if (lt != null) { a.description = lt.getName(); } return a; } public MessageAttribute asMessageAttribute(ChangeMessage message) { MessageAttribute a = new MessageAttribute(); a.timestamp = message.getWrittenOn().getTime() / 1000L; a.reviewer = message.getAuthor() != null ? asAccountAttribute(message.getAuthor()) : asAccountAttribute(myIdent); a.message = message.getMessage(); return a; } public PatchSetCommentAttribute asPatchSetLineAttribute(PatchLineComment c) { PatchSetCommentAttribute a = new PatchSetCommentAttribute(); a.reviewer = asAccountAttribute(c.getAuthor()); a.file = c.getKey().getParentKey().get(); a.line = c.getLine(); a.message = c.getMessage(); return a; } /** Get a link to the change; null if the server doesn't know its own address. */ private String getChangeUrl(final Change change) { if (change != null && urlProvider.get() != null) { final StringBuilder r = new StringBuilder(); r.append(urlProvider.get()); r.append(change.getChangeId()); return r.toString(); } return null; } }