570 lines
20 KiB
Java
570 lines
20 KiB
Java
// 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.get();
|
|
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.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) {
|
|
if (account == null) {
|
|
return null;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|