Java tutorial
// Copyright (C) 2012 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.gerrit.common.data.SubmitRecord.Status.OK; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.gerrit.common.data.SubmitRecord; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.ChangeMessage; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.ProjectUtil; import com.google.gerrit.server.change.Submit.Input; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.MergeQueue; import com.google.gerrit.server.index.ChangeIndexer; import com.google.gerrit.server.project.ChangeControl; import com.google.gwtorm.server.AtomicUpdate; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import org.eclipse.jgit.errors.RepositoryNotFoundException; import java.io.IOException; import java.sql.Timestamp; import java.util.Collection; import java.util.Collections; import java.util.List; public class Submit implements RestModifyView<RevisionResource, Input> { public static class Input { public boolean waitForMerge; } public enum Status { SUBMITTED, MERGED; } public static class Output { public Status status; transient Change change; private Output(Status s, Change c) { status = s; change = c; } } private final Provider<ReviewDb> dbProvider; private final GitRepositoryManager repoManager; private final MergeQueue mergeQueue; private final ChangeIndexer indexer; @Inject Submit(Provider<ReviewDb> dbProvider, GitRepositoryManager repoManager, MergeQueue mergeQueue, ChangeIndexer indexer) { this.dbProvider = dbProvider; this.repoManager = repoManager; this.mergeQueue = mergeQueue; this.indexer = indexer; } @Override public Output apply(RevisionResource rsrc, Input input) throws AuthException, ResourceConflictException, RepositoryNotFoundException, IOException, OrmException { ChangeControl control = rsrc.getControl(); IdentifiedUser caller = (IdentifiedUser) control.getCurrentUser(); Change change = rsrc.getChange(); if (!control.canSubmit()) { throw new AuthException("submit not permitted"); } else if (!change.getStatus().isOpen()) { throw new ResourceConflictException("change is " + status(change)); } else if (!ProjectUtil.branchExists(repoManager, change.getDest())) { throw new ResourceConflictException( String.format("destination branch \"%s\" not found.", change.getDest().get())); } else if (!rsrc.getPatchSet().getId().equals(change.currentPatchSetId())) { // TODO Allow submitting non-current revision by changing the current. throw new ResourceConflictException( String.format("revision %s is not current revision", rsrc.getPatchSet().getRevision().get())); } checkSubmitRule(rsrc); change = submit(rsrc, caller); if (change == null) { throw new ResourceConflictException( "change is " + status(dbProvider.get().changes().get(rsrc.getChange().getId()))); } if (input.waitForMerge) { mergeQueue.merge(change.getDest()); change = dbProvider.get().changes().get(change.getId()); } else { mergeQueue.schedule(change.getDest()); } if (change == null) { throw new ResourceConflictException("change is deleted"); } switch (change.getStatus()) { case SUBMITTED: return new Output(Status.SUBMITTED, change); case MERGED: return new Output(Status.MERGED, change); case NEW: ChangeMessage msg = getConflictMessage(rsrc); if (msg != null) { throw new ResourceConflictException(msg.getMessage()); } default: throw new ResourceConflictException("change is " + status(change)); } } /** * If the merge was attempted and it failed the system usually writes a * comment as a ChangeMessage and sets status to NEW. Find the relevant * message and return it. */ public ChangeMessage getConflictMessage(RevisionResource rsrc) throws OrmException { final Timestamp before = rsrc.getChange().getLastUpdatedOn(); ChangeMessage msg = Iterables.getFirst(Iterables.filter( Lists.reverse(dbProvider.get().changeMessages().byChange(rsrc.getChange().getId()).toList()), new Predicate<ChangeMessage>() { @Override public boolean apply(ChangeMessage input) { return input.getAuthor() == null && input.getWrittenOn().getTime() >= before.getTime(); } }), null); return msg; } public Change submit(RevisionResource rsrc, IdentifiedUser caller) throws OrmException { final Timestamp timestamp = new Timestamp(System.currentTimeMillis()); Change change = rsrc.getChange(); ReviewDb db = dbProvider.get(); db.changes().beginTransaction(change.getId()); try { approve(rsrc.getPatchSet(), caller, timestamp); change = db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() { @Override public Change update(Change change) { if (change.getStatus().isOpen()) { change.setStatus(Change.Status.SUBMITTED); change.setLastUpdatedOn(timestamp); ChangeUtil.computeSortKey(change); return change; } return null; } }); if (change == null) { return null; } db.commit(); } finally { db.rollback(); } indexer.index(change); return change; } private void approve(PatchSet rev, IdentifiedUser caller, Timestamp timestamp) throws OrmException { PatchSetApproval submit = Iterables.getFirst(Iterables.filter( dbProvider.get().patchSetApprovals().byPatchSetUser(rev.getId(), caller.getAccountId()), new Predicate<PatchSetApproval>() { @Override public boolean apply(PatchSetApproval input) { return input.isSubmit(); } }), null); if (submit == null) { submit = new PatchSetApproval( new PatchSetApproval.Key(rev.getId(), caller.getAccountId(), LabelId.SUBMIT), (short) 1); } submit.setValue((short) 1); submit.setGranted(timestamp); dbProvider.get().patchSetApprovals().upsert(Collections.singleton(submit)); } private void checkSubmitRule(RevisionResource rsrc) throws ResourceConflictException { List<SubmitRecord> results = rsrc.getControl().canSubmit(dbProvider.get(), rsrc.getPatchSet()); Optional<SubmitRecord> ok = findOkRecord(results); if (ok.isPresent()) { // Rules supplied a valid solution. return; } else if (results.isEmpty()) { throw new IllegalStateException( String.format("ChangeControl.canSubmit returned empty list for %s in %s", rsrc.getPatchSet().getId(), rsrc.getChange().getProject().get())); } for (SubmitRecord record : results) { switch (record.status) { case CLOSED: throw new ResourceConflictException("change is closed"); case RULE_ERROR: throw new ResourceConflictException(String.format("rule error: %s", record.errorMessage)); case NOT_READY: StringBuilder msg = new StringBuilder(); for (SubmitRecord.Label lbl : record.labels) { switch (lbl.status) { case OK: case MAY: continue; case REJECT: if (msg.length() > 0) msg.append("; "); msg.append("blocked by " + lbl.label); continue; case NEED: if (msg.length() > 0) msg.append("; "); msg.append("needs " + lbl.label); continue; case IMPOSSIBLE: if (msg.length() > 0) msg.append("; "); msg.append("needs " + lbl.label + " (check project access)"); continue; default: throw new IllegalStateException( String.format("Unsupported SubmitRecord.Label %s for %s in %s", lbl.toString(), rsrc.getPatchSet().getId(), rsrc.getChange().getProject().get())); } } throw new ResourceConflictException(msg.toString()); default: throw new IllegalStateException(String.format("Unsupported SubmitRecord %s for %s in %s", record, rsrc.getPatchSet().getId(), rsrc.getChange().getProject().get())); } } } private static Optional<SubmitRecord> findOkRecord(Collection<SubmitRecord> in) { return Iterables.tryFind(in, new Predicate<SubmitRecord>() { @Override public boolean apply(SubmitRecord input) { return input.status == OK; } }); } private static String status(Change change) { return change != null ? change.getStatus().name().toLowerCase() : "deleted"; } public static class CurrentRevision implements RestModifyView<ChangeResource, Input> { private final Provider<ReviewDb> dbProvider; private final Submit submit; private final ChangeJson json; @Inject CurrentRevision(Provider<ReviewDb> dbProvider, Submit submit, ChangeJson json) { this.dbProvider = dbProvider; this.submit = submit; this.json = json; } @Override public Object apply(ChangeResource rsrc, Input input) throws AuthException, ResourceConflictException, RepositoryNotFoundException, IOException, OrmException { PatchSet ps = dbProvider.get().patchSets().get(rsrc.getChange().currentPatchSetId()); if (ps == null) { throw new ResourceConflictException("current revision is missing"); } else if (!rsrc.getControl().isPatchVisible(ps, dbProvider.get())) { throw new AuthException("current revision not accessible"); } Output out = submit.apply(new RevisionResource(rsrc, ps), input); return json.format(out.change); } } }