blob: b1fb5fe39ac450793da6c1318f79633d4a68164f [file] [log] [blame]
// Copyright (C) 2008 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.client.changes;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.patches.PatchScreen;
import com.google.gerrit.client.ui.InlineHyperlink;
import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
import com.google.gerrit.client.ui.NavigationTable;
import com.google.gerrit.client.ui.PatchLink;
import com.google.gerrit.common.data.PatchSetDetail;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.Patch.ChangeType;
import com.google.gerrit.reviewdb.client.Patch.Key;
import com.google.gerrit.reviewdb.client.Patch.PatchType;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTMLTable.Cell;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.globalkey.client.KeyCommand;
import com.google.gwtexpui.progress.client.ProgressBar;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import java.util.ArrayList;
import java.util.List;
public class PatchTable extends Composite {
public interface PatchValidator {
/**
* Returns true if patch is valid
*
* @param patch
* @return
*/
boolean isValid(Patch patch);
}
public final PatchValidator PREFERENCE_VALIDATOR =
new PatchValidator() {
@Override
public boolean isValid(Patch patch) {
return !((listenablePrefs.get().isSkipDeleted()
&& patch.getChangeType().equals(ChangeType.DELETED))
|| (listenablePrefs.get().isSkipUncommented()
&& patch.getCommentCount() == 0));
}
};
private final FlowPanel myBody;
private PatchSetDetail detail;
private Command onLoadCommand;
private MyTable myTable;
private String savePointerId;
private PatchSet.Id base;
private List<Patch> patchList;
private ListenableAccountDiffPreference listenablePrefs;
private List<ClickHandler> clickHandlers;
private boolean active;
private boolean registerKeys;
public PatchTable(ListenableAccountDiffPreference prefs) {
listenablePrefs = prefs;
myBody = new FlowPanel();
initWidget(myBody);
}
public PatchTable() {
this(new ListenableAccountDiffPreference());
}
public int indexOf(Patch.Key patch) {
for (int i = 0; i < patchList.size(); i++) {
if (patchList.get(i).getKey().equals(patch)) {
return i;
}
}
return -1;
}
public void display(PatchSet.Id base, PatchSetDetail detail) {
this.base = base;
this.detail = detail;
this.patchList = detail.getPatches();
myTable = null;
final DisplayCommand cmd = new DisplayCommand(patchList, base);
if (cmd.execute()) {
cmd.initMeter();
Scheduler.get().scheduleIncremental(cmd);
} else {
cmd.showTable();
}
}
public PatchSet.Id getBase() {
return base;
}
public void setSavePointerId(final String id) {
savePointerId = id;
}
public boolean isLoaded() {
return myTable != null;
}
public void onTableLoaded(final Command cmd) {
if (myTable != null) {
cmd.execute();
} else {
onLoadCommand = cmd;
}
}
public void addClickHandler(final ClickHandler clickHandler) {
if (myTable != null) {
myTable.addClickHandler(clickHandler);
} else {
if (clickHandlers == null) {
clickHandlers = new ArrayList<ClickHandler>(2);
}
clickHandlers.add(clickHandler);
}
}
public void setRegisterKeys(final boolean on) {
registerKeys = on;
if (myTable != null) {
myTable.setRegisterKeys(on);
}
}
public void movePointerTo(final Patch.Key k) {
if (myTable != null) {
myTable.movePointerTo(k);
}
}
public void setActive(boolean active) {
this.active = active;
if (myTable != null) {
myTable.setActive(active);
}
}
public void notifyDraftDelta(final Patch.Key k, final int delta) {
if (myTable != null) {
myTable.notifyDraftDelta(k, delta);
}
}
private void setMyTable(MyTable table) {
myBody.clear();
myBody.add(table);
myTable = table;
if (clickHandlers != null) {
for (ClickHandler ch : clickHandlers) {
myTable.addClickHandler(ch);
}
clickHandlers = null;
}
if (active) {
myTable.setActive(true);
active = false;
}
if (registerKeys) {
myTable.setRegisterKeys(registerKeys);
registerKeys = false;
}
myTable.finishDisplay();
}
/**
* @return a link to the previous file in this patch set, or null.
*/
public InlineHyperlink getPreviousPatchLink(int index,
PatchScreen.Type patchType) {
int previousPatchIndex = getPreviousPatch(index, PREFERENCE_VALIDATOR);
if (previousPatchIndex < 0) {
return null;
}
InlineHyperlink link =
createLink(previousPatchIndex, patchType,
SafeHtml.asis(Util.C.prevPatchLinkIcon()), null);
return link;
}
/**
* @return a link to the next file in this patch set, or null.
*/
public InlineHyperlink getNextPatchLink(int index, PatchScreen.Type patchType) {
int nextPatchIndex = getNextPatch(index, false, PREFERENCE_VALIDATOR);
if (nextPatchIndex < 0) {
return null;
}
InlineHyperlink link =
createLink(nextPatchIndex, patchType, null,
SafeHtml.asis(Util.C.nextPatchLinkIcon()));
return link;
}
/**
* @return a link to the the given patch.
* @param index The patch to link to
* @param patchType The type of patch display
* @param before A string to display at the beginning of the href text
* @param after A string to display at the end of the href text
*/
public PatchLink createLink(int index, PatchScreen.Type patchType,
SafeHtml before, SafeHtml after) {
Patch patch = patchList.get(index);
Key thisKey = patch.getKey();
PatchLink link;
if (patchType == PatchScreen.Type.SIDE_BY_SIDE) {
link = new PatchLink.SideBySide("", base, thisKey, index, detail, this);
} else {
link = new PatchLink.Unified("", base, thisKey, index, detail, this);
}
SafeHtmlBuilder text = new SafeHtmlBuilder();
text.append(before);
text.append(getFileNameOnly(patch));
text.append(after);
SafeHtml.set(link, text);
return link;
}
private static String getFileNameOnly(Patch patch) {
// Note: use '/' here and not File.pathSeparator since git paths
// are always separated by /
//
String fileName = getDisplayFileName(patch);
int s = fileName.lastIndexOf('/');
if (s >= 0) {
fileName = fileName.substring(s + 1);
}
return fileName;
}
public static String getDisplayFileName(Patch patch) {
return getDisplayFileName(patch.getKey());
}
public static String getDisplayFileName(Patch.Key patchKey) {
if (Patch.COMMIT_MSG.equals(patchKey.get())) {
return Util.C.commitMessage();
}
return patchKey.get();
}
/**
* Update the reviewed status for the given patch.
*/
public void updateReviewedStatus(Patch.Key patchKey, boolean reviewed) {
if (myTable != null) {
myTable.updateReviewedStatus(patchKey, reviewed);
}
}
public ListenableAccountDiffPreference getPreferences() {
return listenablePrefs;
}
private class MyTable extends NavigationTable<Patch> {
private static final int C_PATH = 2;
private static final int C_DRAFT = 3;
private static final int C_SIZE = 4;
private static final int C_SIDEBYSIDE = 5;
private int activeRow = -1;
MyTable() {
keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.patchTablePrev()));
keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.patchTableNext()));
keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.patchTableOpenDiff()));
keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, Util.C
.patchTableOpenDiff()));
keysNavigation.add(new OpenUnifiedDiffKeyCommand(0, 'O', Util.C
.patchTableOpenUnifiedDiff()));
table.addClickHandler(new ClickHandler() {
@Override
public void onClick(final ClickEvent event) {
final Cell cell = table.getCellForEvent(event);
if (cell != null && cell.getRowIndex() > 0) {
movePointerTo(cell.getRowIndex());
}
}
});
setSavePointerId(PatchTable.this.savePointerId);
}
public void addClickHandler(final ClickHandler clickHandler) {
table.addClickHandler(clickHandler);
}
void updateReviewedStatus(final Patch.Key patchKey, boolean reviewed) {
final int row = findRow(patchKey);
if (0 <= row) {
final Patch patch = getRowItem(row);
if (patch != null) {
patch.setReviewedByCurrentUser(reviewed);
int col = C_SIDEBYSIDE + 2;
if (patch.getPatchType() == Patch.PatchType.BINARY) {
col = C_SIDEBYSIDE + 3;
}
if (reviewed) {
table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
} else {
table.clearCell(row, col);
}
}
}
}
void notifyDraftDelta(final Patch.Key key, final int delta) {
final int row = findRow(key);
if (0 <= row) {
final Patch p = getRowItem(row);
if (p != null) {
p.setDraftCount(p.getDraftCount() + delta);
final SafeHtmlBuilder m = new SafeHtmlBuilder();
appendCommentCount(m, p);
SafeHtml.set(table, row, C_DRAFT, m);
}
}
}
@Override
public void resetHtml(final SafeHtml html) {
super.resetHtml(html);
}
@Override
public void movePointerTo(Object oldId) {
super.movePointerTo(oldId);
}
/** Activates / Deactivates the key navigation and the highlighting of the current row for this table */
public void setActive(boolean active) {
if (active) {
if(activeRow > 0 && getCurrentRow() != activeRow) {
super.movePointerTo(activeRow);
activeRow = -1;
}
} else {
if(getCurrentRow() > 0) {
activeRow = getCurrentRow();
super.movePointerTo(-1);
}
}
setRegisterKeys(active);
}
void initializeRow(int row) {
Patch patch = PatchTable.this.patchList.get(row - 1);
setRowItem(row, patch);
Widget nameCol;
nameCol = new PatchLink.SideBySide(getDisplayFileName(patch), base,
patch.getKey(), row - 1, detail, PatchTable.this);
if (patch.getSourceFileName() != null) {
final String text;
if (patch.getChangeType() == Patch.ChangeType.RENAMED) {
text = Util.M.renamedFrom(patch.getSourceFileName());
} else if (patch.getChangeType() == Patch.ChangeType.COPIED) {
text = Util.M.copiedFrom(patch.getSourceFileName());
} else {
text = Util.M.otherFrom(patch.getSourceFileName());
}
final Label line = new Label(text);
line.setStyleName(Gerrit.RESOURCES.css().sourceFilePath());
final FlowPanel cell = new FlowPanel();
cell.add(nameCol);
cell.add(line);
nameCol = cell;
}
table.setWidget(row, C_PATH, nameCol);
int C_UNIFIED = C_SIDEBYSIDE + 1;
PatchLink sideBySide =
new PatchLink.SideBySide(Util.C.patchTableDiffSideBySide(), base,
patch.getKey(), row - 1, detail, PatchTable.this);
sideBySide.setStyleName("gwt-Anchor");
PatchLink unified =
new PatchLink.Unified(Util.C.patchTableDiffUnified(), base,
patch.getKey(), row - 1, detail, PatchTable.this);
unified.setStyleName("gwt-Anchor");
table.setWidget(row, C_SIDEBYSIDE, sideBySide);
table.setWidget(row, C_UNIFIED, unified);
}
void initializeLastRow(int row) {
Anchor sideBySide = new Anchor(Util.C.diffAllSideBySide());
sideBySide.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
for (Patch p : detail.getPatches()) {
openWindow(Dispatcher.toPatchSideBySide(base, p.getKey()));
}
}
});
table.setWidget(row, C_SIDEBYSIDE - 2, sideBySide);
int C_UNIFIED = C_SIDEBYSIDE - 2 + 1;
Anchor unified = new Anchor(Util.C.diffAllUnified());
unified.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
for (Patch p : detail.getPatches()) {
openWindow(Dispatcher.toPatchUnified(base, p.getKey()));
}
};
});
table.setWidget(row, C_UNIFIED, unified);
}
private void openWindow(String token) {
String url = Window.Location.getPath() + "#" + token;
Window.open(url, "_blank", null);
}
void appendHeader(final SafeHtmlBuilder m) {
m.openTr();
// Cursor
m.openTd();
m.addStyleName(Gerrit.RESOURCES.css().iconHeader());
m.addStyleName(Gerrit.RESOURCES.css().leftMostCell());
m.nbsp();
m.closeTd();
// Mode
m.openTd();
m.setStyleName(Gerrit.RESOURCES.css().iconHeader());
m.nbsp();
m.closeTd();
// "File path"
m.openTd();
m.setStyleName(Gerrit.RESOURCES.css().dataHeader());
m.append(Util.C.patchTableColumnName());
m.closeTd();
// "Comments"
m.openTd();
m.setStyleName(Gerrit.RESOURCES.css().dataHeader());
m.append(Util.C.patchTableColumnComments());
m.closeTd();
// "Size"
m.openTd();
m.setStyleName(Gerrit.RESOURCES.css().dataHeader());
m.append(Util.C.patchTableColumnSize());
m.closeTd();
// "Diff"
m.openTd();
m.setStyleName(Gerrit.RESOURCES.css().dataHeader());
m.setAttribute("colspan", 3);
m.append(Util.C.patchTableColumnDiff());
m.closeTd();
// "Reviewed"
if (Gerrit.isSignedIn()) {
m.openTd();
m.setStyleName(Gerrit.RESOURCES.css().iconHeader());
m.addStyleName(Gerrit.RESOURCES.css().dataHeader());
m.append(Util.C.reviewed());
m.closeTd();
}
m.closeTr();
}
void appendRow(final SafeHtmlBuilder m, final Patch p,
final boolean isReverseDiff) {
m.openTr();
m.openTd();
m.addStyleName(Gerrit.RESOURCES.css().iconCell());
m.addStyleName(Gerrit.RESOURCES.css().leftMostCell());
m.nbsp();
m.closeTd();
m.openTd();
m.setStyleName(Gerrit.RESOURCES.css().changeTypeCell());
if (isReverseDiff) {
m.addStyleName(Gerrit.RESOURCES.css().patchCellReverseDiff());
}
if (Patch.COMMIT_MSG.equals(p.getFileName())) {
m.nbsp();
} else {
m.append(p.getChangeType().getCode());
}
m.closeTd();
m.openTd();
m.addStyleName(Gerrit.RESOURCES.css().dataCell());
m.addStyleName(Gerrit.RESOURCES.css().filePathCell());
m.closeTd();
m.openTd();
m.addStyleName(Gerrit.RESOURCES.css().dataCell());
m.addStyleName(Gerrit.RESOURCES.css().commentCell());
appendCommentCount(m, p);
m.closeTd();
m.openTd();
m.addStyleName(Gerrit.RESOURCES.css().dataCell());
m.addStyleName(Gerrit.RESOURCES.css().patchSizeCell());
if (isReverseDiff) {
m.addStyleName(Gerrit.RESOURCES.css().patchCellReverseDiff());
}
appendSize(m, p);
m.closeTd();
// Diff
openlink(m, 2);
m.closeTd();
openlink(m, 1);
m.closeTd();
// Green check mark if the user is logged in and they reviewed that file
if (Gerrit.isSignedIn()) {
m.openTd();
m.setStyleName(Gerrit.RESOURCES.css().dataCell());
if (p.isReviewedByCurrentUser()) {
m.openDiv();
m.setStyleName(Gerrit.RESOURCES.css().greenCheckClass());
m.closeSelf();
}
m.closeTd();
}
m.closeTr();
}
void appendLastRow(final SafeHtmlBuilder m, int ins, int dels,
final boolean isReverseDiff) {
m.openTr();
m.openTd();
m.addStyleName(Gerrit.RESOURCES.css().iconCell());
m.addStyleName(Gerrit.RESOURCES.css().noborder());
m.nbsp();
m.closeTd();
m.openTd();
m.setAttribute("colspan", C_SIZE - 1);
m.closeTd();
m.openTd();
m.addStyleName(Gerrit.RESOURCES.css().dataCell());
m.addStyleName(Gerrit.RESOURCES.css().patchSizeCell());
m.addStyleName(Gerrit.RESOURCES.css().leftMostCell());
if (isReverseDiff) {
m.addStyleName(Gerrit.RESOURCES.css().patchCellReverseDiff());
}
m.append(Util.M.patchTableSize_Modify(ins, dels));
m.closeTd();
openlink(m, 2);
m.closeTd();
openlink(m, 1);
m.closeTd();
m.closeTr();
}
void appendCommentCount(final SafeHtmlBuilder m, final Patch p) {
if (p.getCommentCount() > 0) {
m.append(Util.M.patchTableComments(p.getCommentCount()));
}
if (p.getDraftCount() > 0) {
if (p.getCommentCount() > 0) {
m.append(", ");
}
m.openSpan();
m.setStyleName(Gerrit.RESOURCES.css().drafts());
m.append(Util.M.patchTableDrafts(p.getDraftCount()));
m.closeSpan();
}
}
void appendSize(final SafeHtmlBuilder m, final Patch p) {
if (Patch.COMMIT_MSG.equals(p.getFileName())) {
m.nbsp();
return;
}
if (p.getPatchType() == PatchType.UNIFIED) {
int ins = p.getInsertions();
int dels = p.getDeletions();
switch (p.getChangeType()) {
case ADDED:
m.append(Util.M.patchTableSize_Lines(ins));
break;
case DELETED:
m.nbsp();
break;
case MODIFIED:
case COPIED:
case RENAMED:
m.append(Util.M.patchTableSize_Modify(ins, dels));
break;
case REWRITE:
break;
}
} else {
m.nbsp();
}
}
private void openlink(final SafeHtmlBuilder m, final int colspan) {
m.openTd();
m.addStyleName(Gerrit.RESOURCES.css().dataCell());
m.addStyleName(Gerrit.RESOURCES.css().diffLinkCell());
m.setAttribute("colspan", colspan);
}
@Override
protected Object getRowItemKey(final Patch item) {
return item.getKey();
}
@Override
protected void onOpenRow(final int row) {
Widget link = table.getWidget(row, C_PATH);
if (link instanceof FlowPanel) {
link = ((FlowPanel) link).getWidget(0);
}
if (link instanceof InlineHyperlink) {
((InlineHyperlink) link).go();
}
}
private final class OpenUnifiedDiffKeyCommand extends KeyCommand {
public OpenUnifiedDiffKeyCommand(int mask, char key, String help) {
super(mask, key, help);
}
@Override
public void onKeyPress(KeyPressEvent event) {
Widget link = table.getWidget(getCurrentRow(), C_PATH);
if (link instanceof FlowPanel) {
link = ((FlowPanel) link).getWidget(0);
}
if (link instanceof PatchLink.Unified) {
((InlineHyperlink) link).go();
} else {
link = table.getWidget(getCurrentRow(), C_SIDEBYSIDE + 1);
if (link instanceof PatchLink.Unified) {
((InlineHyperlink) link).go();
}
}
}
}
}
private final class DisplayCommand implements RepeatingCommand {
private final MyTable table;
private final List<Patch> list;
private boolean attached;
private SafeHtmlBuilder nc = new SafeHtmlBuilder();
private int stage = 0;
private int row;
private double start;
private ProgressBar meter;
private int insertions;
private int deletions;
private final PatchSet.Id psIdToCompareWith;
private DisplayCommand(final List<Patch> list, final PatchSet.Id psIdToCompareWith) {
this.table = new MyTable();
this.list = list;
this.psIdToCompareWith = psIdToCompareWith;
}
/**
* Add the files contained in the list of patches to the table, one per row.
*/
@SuppressWarnings("fallthrough")
public boolean execute() {
final boolean attachedNow = isAttached();
if (!attached && attachedNow) {
// Remember that we have been attached at least once. If
// later we find we aren't attached we should stop running.
//
attached = true;
} else if (attached && !attachedNow) {
// If the user navigated away, we aren't in the DOM anymore.
// Don't continue to render.
//
return false;
}
boolean isReverseDiff = false;
if (psIdToCompareWith != null
&& list.get(0).getKey().getParentKey().get() < psIdToCompareWith.get()) {
isReverseDiff = true;
}
start = System.currentTimeMillis();
switch (stage) {
case 0:
if (row == 0) {
table.appendHeader(nc);
table.appendRow(nc, list.get(row++), isReverseDiff);
}
while (row < list.size()) {
Patch p = list.get(row);
insertions += p.getInsertions();
deletions += p.getDeletions();
table.appendRow(nc, p, isReverseDiff);
if ((++row % 10) == 0 && longRunning()) {
updateMeter();
return true;
}
}
table.appendLastRow(nc, insertions, deletions, isReverseDiff);
table.resetHtml(nc);
table.initializeLastRow(row + 1);
nc = null;
stage = 1;
row = 0;
case 1:
while (row < list.size()) {
table.initializeRow(row + 1);
if ((++row % 50) == 0 && longRunning()) {
updateMeter();
return true;
}
}
updateMeter();
showTable();
}
return false;
}
void showTable() {
setMyTable(table);
if (PatchTable.this.onLoadCommand != null) {
PatchTable.this.onLoadCommand.execute();
PatchTable.this.onLoadCommand = null;
}
}
void initMeter() {
if (meter == null) {
meter = new ProgressBar(Util.M.loadingPatchSet(detail.getPatchSet().getId().get()));
PatchTable.this.myBody.clear();
PatchTable.this.myBody.add(meter);
}
updateMeter();
}
void updateMeter() {
if (meter != null) {
final int n = list.size();
meter.setValue(((100 * (stage * n + row)) / (2 * n)));
}
}
private boolean longRunning() {
return System.currentTimeMillis() - start > 200;
}
}
/**
* Gets the next patch
*
* @param currentIndex
* @param validators
* @param loopAround loops back around to the front and traverses if this is
* true
* @return
*/
public int getNextPatch(int currentIndex, boolean loopAround,
PatchValidator... validators) {
return getNextPatchHelper(currentIndex, loopAround, detail.getPatches()
.size(), validators);
}
/**
* Helper function for getNextPatch
*
* @param currentIndex
* @param validators
* @param loopAround
* @param maxIndex will only traverse up to this index
* @return
*/
private int getNextPatchHelper(int currentIndex, boolean loopAround,
int maxIndex, PatchValidator... validators) {
for (int i = currentIndex + 1; i < maxIndex; i++) {
Patch patch = detail.getPatches().get(i);
if (patch != null && patchIsValid(patch, validators)) {
return i;
}
}
if (loopAround) {
return getNextPatchHelper(-1, false, currentIndex, validators);
}
return -1;
}
/**
* @return the index to the previous patch
*/
public int getPreviousPatch(int currentIndex, PatchValidator... validators) {
for (int i = currentIndex - 1; i >= 0; i--) {
Patch patch = detail.getPatches().get(i);
if (patch != null && patchIsValid(patch, validators)) {
return i;
}
}
return -1;
}
/**
* Helper function that returns whether a patch is valid or not
*
* @param patch
* @param validators
* @return whether the patch is valid based on the validators
*/
private boolean patchIsValid(Patch patch, PatchValidator... validators) {
for (PatchValidator v : validators) {
if (!v.isValid(patch)) {
return false;
}
}
return true;
}
}