diff --git a/Documentation/index.txt b/Documentation/index.txt index 856c974eef..1e13e8d8f5 100644 --- a/Documentation/index.txt +++ b/Documentation/index.txt @@ -5,6 +5,7 @@ User Guide ---------- * link:http://source.android.com/submit-patches/workflow[Default Workflow] +* link:user-search.html[Searching Changes] * link:cmd-index.html[Command Line Tools] * link:pgm-index.html[Server Programs] * link:user-upload.html[Uploading Changes] diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt new file mode 100644 index 0000000000..30d26f0bff --- /dev/null +++ b/Documentation/user-search.txt @@ -0,0 +1,331 @@ +Gerrit Code Review - Searching Changes +====================================== + +Default Searches +---------------- + +Most basic searches can be viewed by clicking on a link along the top +menu bar. The link will prefill the search box with a common search +query, execute it, and present the results. If exactly one change +matches the search, the change will be presented instead of a list. + + +[grid="all"] +`---------------------------`------------------------------ +Description Default Query +----------------------------------------------------------- +All > Open status:open '(or is:open)' +All > Merged status:merged +All > Abandoned status:abandoned +My > Dafts has:draft +My > Watched Changes status:open is:watched +My > Starred Changes is:starred +Open changes in Foo status:open project:Foo +----------------------------------------------------------- + +Basic Change Search +------------------- + +Similar to many popular search engines on the web, just enter some +text and let Gerrit figure out the meaning: + +[grid="all"] +`---------------------------------`------------------------------ +Description Examples +----------------------------------------------------------------- +Legacy numerical id 15183 +Full or abbreviated Change-Id Ic0ff33 +Full or abbreviated commit SHA-1 d81b32ef +Email address user@example.com +Approval requirement CodeReview>=+2, Verified=1 +----------------------------------------------------------------- + + +Search Operators +---------------- + +Operators act as restrictions on the search. As more operators +are added to the same query string, they further restrict the +returned results. + +[[change]] +change:'ID':: ++ +Either a legacy numerical 'ID' such as 15183, or a newer style +Change-Id that was scraped out of the commit message. + +[[owner]] +owner:'USER':: ++ +Changes originally submitted by 'USER'. + +[[reviewer]] +reviewer:'USER':: ++ +Changes that have been, or need to be, reviewed by 'USER'. + +[[commit]] +commit:'SHA1':: ++ +Changes where 'SHA1' is one of the patch sets of the change. + +[[project]] +project:'PROJECT':: ++ +Changes occuring in 'PROJECT'. + +[[branch]] +branch:'BRANCH':: ++ +Changes for 'BRANCH'. The branch name is the short name shown +in the web interface, without the traditional 'refs/heads/' +prefix. This operator is a shorthand for 'refs:'. Searching for +'branch:master' really means 'ref:refs/heads/master', and searching +for 'branch:refs/heads/master' is the same as searching for +'ref:refs/heads/refs/heads/master'. + +[[topic]] +topic:'TOPIC':: ++ +Changes whose designated topic at upload was 'TOPIC'. This is +often combined with 'branch:' and 'project:' operators to select +all related changes in a series. + +[[ref]] +ref:'REF':: ++ +Changes where the destination branch is exactly the given 'REF' +name. Since 'REF' is absolute from the top of the repository it +must start with 'refs/'. + +[[tr]][[bug]] +tr:'ID', bug:'ID':: ++ +Search for changes whose commit message contains 'ID' and matched +one or more of the +link:config-gerrit.html#trackingid[trackingid sections] +in the server's configuration file. This is typically used to +search for changes that fix a bug or defect by the issue tracking +system's issue identifier. + +[[label]] +label:'VALUE':: ++ +Matches changes where the approval score 'VALUE' has been set during +a review. See <> below for more detail on the format +of the argument. + +[[has]] +has:draft:: ++ +True if there is a draft comment saved by the current user. + +has:star:: ++ +Same as 'is:starred', true if the change has been starred by the +current user. + +[[is]] +is:starred:: ++ +Same as 'has:star', true if the change has been starred by the +current user. + +is:watched:: ++ +True if this change matches one of the current user's watch filters, +and thus is likely to notify the user when it updates. + +is:reviewed:: ++ +True if there is at least one non-zero score on the change, in any +approval category, by any user. + +is:open:: ++ +True if the change is other open or submitted, merge pending. + +is:closed:: ++ +True if the change is either merged or abandoned. + +is:submitted, is:merged, is:abandoned:: ++ +Same as <>. + +[[status]] +status:open:: ++ +True if the change state is other 'review in progress' or 'submitted, +merge pending'. + +status:reviewed:: ++ +Same as 'is:reviewed', matches if there is at least one non-zero +score on the change, in any approval category, by any user. + +status:submitted:: ++ +Change has been submitted, but is waiting for a dependency. + +status:closed:: ++ +True if the change is either 'merged' or 'abandoned'. + +status:merged:: ++ +Change has been merged into the branch. + +status:abandoned:: ++ +Change has been abandoned by the change owner, or administrator. + + +Boolean Operators +----------------- + +Unless otherwise specified, operators are joined using the `AND` +boolean operator, thereby restricting the search results. + +Parentheses can be used to force a particular precendence on complex +operator expressions, otherwise OR has higher precendence than AND. + +Negation +~~~~~~~~ +Any operator can be negated by prefixing it with `-`, for example +`-is:starred` is the exact opposite of `is:starred` and will +therefore return changes that are *not* starred by the current user. + +The operator `NOT` (in all caps) is a synonym. + +AND +~~~ +The boolean operator `AND` (in all caps) can be used to join two +other operators together. This results in a restriction of the +results, returning only changes that match both operators. + +OR +~~ +The boolean operator `OR` (in all caps) can be used to find changes +that match either operator. This increases the nubmer of results +that are returned, as more changes are considered. + + +[[labels]] +Labels +------ +Label operators can be used to match approval score given during +a code review. The specific set of supported labels depends on +the server configuration, however `CodeReview` and `Verified` +are the default labels provided out of the box. + +A label name is any of the following: + +* The category name. If the category name contains spaces, + it must be wrapped in double quotes. Example: `label:"Code Review"`. + +* The name, without spaces. This avoids needing to use double quotes + for the common category Code Review. Example: `label:CodeReview`. + +* The internal short name. Example: `label:CRVW`, or `label:VRIF`. + +* The one or two character abbreviation shown in the column header + of change list pages. Example: `label:R` or `label:V`. + +A label name must be followed by a score, or an operator and a score. +The easiest way to explain these are by example. + +`label:CodeReview=2`:: +`label:CodeReview=+2`:: +`label:CodeReview+2`:: ++ +Matches changes where there is at least one \+2 score for Code Review. +The \+ prefix is optional for positive score values. If the + is used, +the = operator is optional. + +`label:CodeReview=-2`:: +`label:CodeReview-2`:: ++ +Matches changes where there is at least one -2 score for Code Review. +Because the negative sign is required, the = operator is optional. + +`label:CodeReview=1`:: ++ +Matches changes where there is at least one +1 score for Code Review. +Scores of +2 are not matched, even though they are higher. + +`label:CodeReview>=1`:: ++ +Matches changes with either a +1, +2, or any higher score. + +`label:CodeReview<=-1`:: ++ +Matches changes with either a -1, -2, or any lower score. + +`is:open CodeReview+2 Verified+1 -Verified-1 -CodeReview-2`:: ++ +Matches changes that are ready to be submitted. + +`is:open (Verified-1 OR CodeReview-2)`:: ++ +Changes that are blocked from submission due to a blocking score. + + +Magical Operators +----------------- + +Most of these operators exist to support features of Gerrit Code +Review, and are not meant to be accessed by the average end-user. +However, they are recognized by the query parser, and may prove +useful in limited contexts to administrators or power-users. + +visibleto:'USER-or-GROUP':: ++ +Matches changes that are visible to 'USER' or to anyone who is a +member of 'GROUP'. Here group names may be specified as either +an internal group name, or if LDAP is being used, an external LDAP +group name. The value may be wrapped in double quotes to include +spaces or other special characters. For example, to match an LDAP +group: `visibleto:"CN=Developers, DC=example, DC=com"`. ++ +This operator may be useful to test access control rules, however a +change can only be matched if both the current user and the supplied +user or group can see it. This is due to the implicit 'is:visible' +clause that is always added by the server. + +is:visible:: ++ +Magical internal flag to prove the current user has access to read +the change. This flag is always added to any query. + +starredby:'USER':: ++ +Matches changes that have been started by 'USER'. + +watchedby:'USER':: ++ +Matches changes that 'USER' has configured watch filters for. + +draftby:'USER':: ++ +Matches changes that 'USER' has left unpublished drafts on. +Since the drafts are unpublished, it is not possible to see the +draft text, or even how many drafts there are. + +limit:'CNT':: ++ +Limit the returned results to no more than 'CNT' records. This is +automatically set to the page size configured in the current user's +preferences. Including it in a web query may lead to unpredictable +results with regards to pagination. + +sortkey\_after:'KEY', sortkey\_before:'KEY':: ++ +Restart the low level scan routine from 'KEY'. This is automatically +set by the pagination system as the user navigates through results +of a query. Including either value in a web query may lead to +unpredictable results. + +GERRIT +------ +Part of link:index.html[Gerrit Code Review] diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java index 6f6fc09fa5..5ff85e3a2a 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java @@ -17,7 +17,6 @@ package com.google.gerrit.common.data; import com.google.gerrit.common.auth.SignInRequired; import com.google.gerrit.reviewdb.Account; import com.google.gerrit.reviewdb.Change; -import com.google.gerrit.reviewdb.Project; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwtjsonrpc.client.RemoteJsonService; import com.google.gwtjsonrpc.client.RpcImpl; @@ -28,52 +27,6 @@ import java.util.Set; @RpcImpl(version = Version.V2_0) public interface ChangeListService extends RemoteJsonService { - /** Get all open changes more recent than pos, fetching at most limit rows. */ - void allOpenPrev(String pos, int limit, - AsyncCallback callback); - - /** Get all open changes older than pos, fetching at most limit rows. */ - void allOpenNext(String pos, int limit, - AsyncCallback callback); - - @SignInRequired - void myWatchedOpenPrev(String pos, int limit, - AsyncCallback callback); - - @SignInRequired - void myWatchedOpenNext(String pos, int limit, - AsyncCallback callback); - - /** Get all open changes more recent than pos, fetching at most limit rows. */ - void byProjectOpenPrev(Project.NameKey project, String pos, int limit, - AsyncCallback callback); - - /** Get all open changes older than pos, fetching at most limit rows. */ - void byProjectOpenNext(Project.NameKey project, String pos, int limit, - AsyncCallback callback); - - /** - * Get all closed changes with same status, more recent than pos, fetching at - * most limit rows. - */ - void byProjectClosedPrev(Project.NameKey project, Change.Status status, - String pos, int limit, AsyncCallback callback); - - /** - * Get all closed changes with same status, older than pos, fetching at most - * limit rows. - */ - void byProjectClosedNext(Project.NameKey project, Change.Status status, - String pos, int limit, AsyncCallback callback); - - /** Get all closed changes more recent than pos, fetching at most limit rows. */ - void allClosedPrev(Change.Status status, String pos, int limit, - AsyncCallback callback); - - /** Get all closed changes older than pos, fetching at most limit rows. */ - void allClosedNext(Change.Status status, String pos, int limit, - AsyncCallback callback); - /** Get all changes which match an arbitrary query string. */ void allQueryPrev(String query, String pos, int limit, AsyncCallback callback); @@ -85,14 +38,6 @@ public interface ChangeListService extends RemoteJsonService { /** Get the data to show AccountDashboardScreen for an account. */ void forAccount(Account.Id id, AsyncCallback callback); - /** Get the changes starred by the caller. */ - @SignInRequired - void myStarredChanges(AsyncCallback callback); - - /** Get the changes with unpublished drafts by the caller. */ - @SignInRequired - void myDraftChanges(AsyncCallback callback); - /** Get the ids of all changes starred by the caller. */ @SignInRequired void myStarredChangeIds(AsyncCallback> callback); diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java new file mode 100644 index 0000000000..4a66a416a7 --- /dev/null +++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java @@ -0,0 +1,24 @@ +// Copyright (C) 2009 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.common.errors; + +/** Error indicating the query cannot be executed. */ +public class InvalidQueryException extends Exception { + private static final long serialVersionUID = 1L; + + public InvalidQueryException(String message, String query) { + super("Invalid query: " + query + "\n\n" + message); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java index 099939543e..5c07a8050d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java @@ -30,7 +30,6 @@ import static com.google.gerrit.common.PageLinks.SETTINGS_PREFERENCES; import static com.google.gerrit.common.PageLinks.SETTINGS_PROJECTS; import static com.google.gerrit.common.PageLinks.SETTINGS_SSHKEYS; import static com.google.gerrit.common.PageLinks.SETTINGS_WEBIDENT; -import static com.google.gerrit.common.PageLinks.TOP; import com.google.gerrit.client.account.MyAgreementsScreen; import com.google.gerrit.client.account.MyContactInformationScreen; @@ -54,19 +53,10 @@ import com.google.gerrit.client.admin.ProjectScreen; import com.google.gerrit.client.auth.openid.OpenIdSignInDialog; import com.google.gerrit.client.auth.userpass.UserPassSignInDialog; import com.google.gerrit.client.changes.AccountDashboardScreen; -import com.google.gerrit.client.changes.AllAbandonedChangesScreen; -import com.google.gerrit.client.changes.AllMergedChangesScreen; -import com.google.gerrit.client.changes.AllOpenChangesScreen; -import com.google.gerrit.client.changes.ByProjectAbandonedChangesScreen; -import com.google.gerrit.client.changes.ByProjectMergedChangesScreen; -import com.google.gerrit.client.changes.ByProjectOpenChangesScreen; -import com.google.gerrit.client.changes.ChangeQueryResultsScreen; import com.google.gerrit.client.changes.ChangeScreen; -import com.google.gerrit.client.changes.MineDraftsScreen; -import com.google.gerrit.client.changes.MineStarredScreen; -import com.google.gerrit.client.changes.MineWatchedOpenChangesScreen; import com.google.gerrit.client.changes.PatchTable; import com.google.gerrit.client.changes.PublishCommentScreen; +import com.google.gerrit.client.changes.QueryScreen; import com.google.gerrit.client.patches.PatchScreen; import com.google.gerrit.client.ui.Screen; import com.google.gerrit.common.auth.SignInMode; @@ -167,15 +157,15 @@ public class Dispatcher { } } else if (MINE_STARRED.equals(token)) { - return new MineStarredScreen(); + return QueryScreen.forQuery("is:starred"); } else if (MINE_DRAFTS.equals(token)) { - return new MineDraftsScreen(); + return QueryScreen.forQuery("has:draft"); } else { String p = "mine,watched,"; if (token.startsWith(p)) { - return new MineWatchedOpenChangesScreen(skip(p, token)); + return QueryScreen.forQuery("is:watched status:open", skip(p, token)); } return new NotFoundScreen(); @@ -187,17 +177,17 @@ public class Dispatcher { p = "all,abandoned,"; if (token.startsWith(p)) { - return new AllAbandonedChangesScreen(skip(p, token)); + return QueryScreen.forQuery("status:abandoned", skip(p, token)); } p = "all,merged,"; if (token.startsWith(p)) { - return new AllMergedChangesScreen(skip(p, token)); + return QueryScreen.forQuery("status:merged", skip(p, token)); } p = "all,open,"; if (token.startsWith(p)) { - return new AllOpenChangesScreen(skip(p, token)); + return QueryScreen.forQuery("status:open", skip(p, token)); } return new NotFoundScreen(); @@ -210,25 +200,32 @@ public class Dispatcher { if (token.startsWith(p)) { final String s = skip(p, token); final int c = s.indexOf(','); - return new ByProjectOpenChangesScreen(Project.NameKey.parse(s.substring( - 0, c)), s.substring(c + 1)); + Project.NameKey proj = Project.NameKey.parse(s.substring(0, c)); + return QueryScreen.forQuery( // + "status:open " + QueryScreen.op("project", proj.get()), // + s.substring(c + 1)); } p = "project,merged,"; if (token.startsWith(p)) { final String s = skip(p, token); final int c = s.indexOf(','); - return new ByProjectMergedChangesScreen(Project.NameKey.parse(s - .substring(0, c)), s.substring(c + 1)); + Project.NameKey proj = Project.NameKey.parse(s.substring(0, c)); + return QueryScreen.forQuery( // + "status:merged " + QueryScreen.op("project", proj.get()), // + s.substring(c + 1)); } p = "project,abandoned,"; if (token.startsWith(p)) { final String s = skip(p, token); final int c = s.indexOf(','); - return new ByProjectAbandonedChangesScreen(Project.NameKey.parse(s - .substring(0, c)), s.substring(c + 1)); + Project.NameKey proj = Project.NameKey.parse(s.substring(0, c)); + return QueryScreen.forQuery( // + "status:abandoned " + QueryScreen.op("project", proj.get()), // + s.substring(c + 1)); } + return new NotFoundScreen(); } @@ -247,7 +244,7 @@ public class Dispatcher { if (token.startsWith(p)) { final String s = skip(p, token); final int c = s.indexOf(','); - return new ChangeQueryResultsScreen(s.substring(0, c), s.substring(c + 1)); + return new QueryScreen(s.substring(0, c), s.substring(c + 1)); } return new NotFoundScreen(); @@ -380,7 +377,7 @@ public class Dispatcher { } switch (mode) { case SIGN_IN: - return new AllOpenChangesScreen(TOP); + return QueryScreen.forQuery("status:open"); case LINK_IDENTIY: return new MyIdentitiesScreen(); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java index 6535a07f5d..74a2678555 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java @@ -19,6 +19,7 @@ import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.StatusCodeException; import com.google.gwt.user.client.ui.Button; @@ -121,7 +122,10 @@ public class ErrorDialog extends PluginSafePopupPanel { final Label r = new Label(cn); r.setStyleName(Gerrit.RESOURCES.css().errorDialogErrorType()); body.add(r); - body.add(new Label(what.getMessage())); + + final Label m = new Label(what.getMessage()); + DOM.setStyleAttribute(m.getElement(),"whiteSpace","pre"); + body.add(m); } public void setText(final String t) { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java index 9af36af27e..ecb4cd619b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java @@ -443,6 +443,7 @@ public class Gerrit implements EntryPoint { if (getConfig().isDocumentationAvailable()) { m = new LinkMenuBar(); addDocLink(m, C.menuDocumentationIndex(), "index.html"); + addDocLink(m, C.menuDocumentationSearch(), "user-search.html"); addDocLink(m, C.menuDocumentationUpload(), "user-upload.html"); addDocLink(m, C.menuDocumentationAccess(), "access-control.html"); menuLeft.add(m, C.menuDocumentation()); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java index 357fc6fd44..3f263d23fd 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java @@ -64,6 +64,7 @@ public interface GerritConstants extends Constants { String menuDocumentation(); String menuDocumentationIndex(); + String menuDocumentationSearch(); String menuDocumentationUpload(); String menuDocumentationAccess(); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties index a341fa0117..83a06e47de 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties @@ -47,7 +47,8 @@ menuProjects = Projects menuDocumentation = Documentation menuDocumentationIndex = Index -menuDocumentationUpload = Uploading Changes +menuDocumentationSearch = Searching +menuDocumentationUpload = Uploading menuDocumentationAccess = Access Controls searchHint = Change #, SHA-1, tr:id, owner:email or reviewer:email diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllAbandonedChangesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllAbandonedChangesScreen.java deleted file mode 100644 index af8aa6306d..0000000000 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllAbandonedChangesScreen.java +++ /dev/null @@ -1,44 +0,0 @@ -// 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.Gerrit; -import com.google.gerrit.reviewdb.Change; - - -public class AllAbandonedChangesScreen extends PagedSingleListScreen { - public AllAbandonedChangesScreen(final String positionToken) { - super("all,abandoned", positionToken); - } - - @Override - protected void onInitUI() { - super.onInitUI(); - setWindowTitle(Gerrit.C.menuAllAbandoned()); - setPageTitle(Util.C.allAbandonedChanges()); - } - - @Override - protected void loadPrev() { - Util.LIST_SVC.allClosedPrev(Change.Status.ABANDONED, pos, pageSize, - loadCallback()); - } - - @Override - protected void loadNext() { - Util.LIST_SVC.allClosedNext(Change.Status.ABANDONED, pos, pageSize, - loadCallback()); - } -} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllMergedChangesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllMergedChangesScreen.java deleted file mode 100644 index f9523371d7..0000000000 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllMergedChangesScreen.java +++ /dev/null @@ -1,44 +0,0 @@ -// 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.Gerrit; -import com.google.gerrit.reviewdb.Change; - - -public class AllMergedChangesScreen extends PagedSingleListScreen { - public AllMergedChangesScreen(final String positionToken) { - super("all,merged", positionToken); - } - - @Override - protected void onInitUI() { - super.onInitUI(); - setWindowTitle(Gerrit.C.menuAllMerged()); - setPageTitle(Util.C.allMergedChanges()); - } - - @Override - protected void loadPrev() { - Util.LIST_SVC.allClosedPrev(Change.Status.MERGED, pos, pageSize, - loadCallback()); - } - - @Override - protected void loadNext() { - Util.LIST_SVC.allClosedNext(Change.Status.MERGED, pos, pageSize, - loadCallback()); - } -} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllOpenChangesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllOpenChangesScreen.java deleted file mode 100644 index 45171e1a43..0000000000 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AllOpenChangesScreen.java +++ /dev/null @@ -1,42 +0,0 @@ -// 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.Gerrit; - - - -public class AllOpenChangesScreen extends PagedSingleListScreen { - public AllOpenChangesScreen(final String positionToken) { - super("all,open", positionToken); - } - - @Override - protected void onInitUI() { - super.onInitUI(); - setWindowTitle(Gerrit.C.menuAllOpen()); - setPageTitle(Util.C.allOpenChanges()); - } - - @Override - protected void loadPrev() { - Util.LIST_SVC.allOpenPrev(pos, pageSize, loadCallback()); - } - - @Override - protected void loadNext() { - Util.LIST_SVC.allOpenNext(pos, pageSize, loadCallback()); - } -} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectAbandonedChangesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectAbandonedChangesScreen.java deleted file mode 100644 index de74452722..0000000000 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectAbandonedChangesScreen.java +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (C) 2009 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.reviewdb.Change; -import com.google.gerrit.reviewdb.Project; - - -public class ByProjectAbandonedChangesScreen extends PagedSingleListScreen { - private final Project.NameKey projectKey; - - public ByProjectAbandonedChangesScreen(final Project.NameKey proj, - final String positionToken) { - super("project,abandoned," + proj.toString(), positionToken); - projectKey = proj; - } - - @Override - protected void onInitUI() { - super.onInitUI(); - setPageTitle(Util.M.changesAbandonedInProject(projectKey.get())); - } - - @Override - protected void loadPrev() { - Util.LIST_SVC.byProjectClosedPrev(projectKey, Change.Status.ABANDONED, pos, - pageSize, loadCallback()); - } - - @Override - protected void loadNext() { - Util.LIST_SVC.byProjectClosedNext(projectKey, Change.Status.ABANDONED, pos, - pageSize, loadCallback()); - } -} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectMergedChangesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectMergedChangesScreen.java deleted file mode 100644 index 8a2184019d..0000000000 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectMergedChangesScreen.java +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (C) 2009 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.reviewdb.Change; -import com.google.gerrit.reviewdb.Project; - - -public class ByProjectMergedChangesScreen extends PagedSingleListScreen { - private final Project.NameKey projectKey; - - public ByProjectMergedChangesScreen(final Project.NameKey proj, - final String positionToken) { - super("project,merged," + proj.toString(), positionToken); - projectKey = proj; - } - - @Override - protected void onInitUI() { - super.onInitUI(); - setPageTitle(Util.M.changesMergedInProject(projectKey.get())); - } - - @Override - protected void loadPrev() { - Util.LIST_SVC.byProjectClosedPrev(projectKey, Change.Status.MERGED, pos, - pageSize, loadCallback()); - } - - @Override - protected void loadNext() { - Util.LIST_SVC.byProjectClosedNext(projectKey, Change.Status.MERGED, pos, - pageSize, loadCallback()); - } -} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectOpenChangesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectOpenChangesScreen.java deleted file mode 100644 index b55e86b0f7..0000000000 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ByProjectOpenChangesScreen.java +++ /dev/null @@ -1,44 +0,0 @@ -// 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.reviewdb.Project; - - -public class ByProjectOpenChangesScreen extends PagedSingleListScreen { - private final Project.NameKey projectKey; - - public ByProjectOpenChangesScreen(final Project.NameKey proj, - final String positionToken) { - super("project,open," + proj.toString(), positionToken); - projectKey = proj; - } - - @Override - protected void onInitUI() { - super.onInitUI(); - setPageTitle(Util.M.changesOpenInProject(projectKey.get())); - } - - @Override - protected void loadPrev() { - Util.LIST_SVC.byProjectOpenPrev(projectKey, pos, pageSize, loadCallback()); - } - - @Override - protected void loadNext() { - Util.LIST_SVC.byProjectOpenNext(projectKey, pos, pageSize, loadCallback()); - } -} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/MineStarredScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/MineStarredScreen.java deleted file mode 100644 index df4d30d40a..0000000000 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/MineStarredScreen.java +++ /dev/null @@ -1,38 +0,0 @@ -// 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.Gerrit; -import com.google.gerrit.common.PageLinks; - - -public class MineStarredScreen extends MineSingleListScreen { - public MineStarredScreen() { - super(PageLinks.MINE_STARRED); - } - - @Override - protected void onInitUI() { - super.onInitUI(); - setWindowTitle(Gerrit.C.menuMyStarredChanges()); - setPageTitle(Util.C.starredHeading()); - } - - @Override - protected void onLoad() { - super.onLoad(); - Util.LIST_SVC.myStarredChanges(loadCallback()); - } -} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/MineWatchedOpenChangesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/MineWatchedOpenChangesScreen.java deleted file mode 100644 index 8e31b361da..0000000000 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/MineWatchedOpenChangesScreen.java +++ /dev/null @@ -1,41 +0,0 @@ -// 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.client.changes; - -import com.google.gerrit.client.Gerrit; - -public class MineWatchedOpenChangesScreen extends PagedSingleListScreen { - public MineWatchedOpenChangesScreen(final String positionToken) { - super("mine,watched", positionToken); - setRequiresSignIn(true); - } - - @Override - protected void onInitUI() { - super.onInitUI(); - setWindowTitle(Gerrit.C.menuMyWatchedChanges()); - setPageTitle(Util.C.watchedHeading()); - } - - @Override - protected void loadPrev() { - Util.LIST_SVC.myWatchedOpenPrev(pos, pageSize, loadCallback()); - } - - @Override - protected void loadNext() { - Util.LIST_SVC.myWatchedOpenNext(pos, pageSize, loadCallback()); - } -} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeQueryResultsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java similarity index 78% rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeQueryResultsScreen.java rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java index ec4a4a52b8..3ddfeffe51 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeQueryResultsScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java @@ -23,12 +23,25 @@ import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwtorm.client.KeyUtil; +public class QueryScreen extends PagedSingleListScreen { + public static String op(String name, String value) { + if (value.indexOf(' ') >= 0) { + return name + ":\"" + value + "\""; + } + return name + ":" + value; + } + + public static QueryScreen forQuery(String query) { + return forQuery(query, PageLinks.TOP); + } + + public static QueryScreen forQuery(String query, String position) { + return new QueryScreen(KeyUtil.encode(query), position); + } -public class ChangeQueryResultsScreen extends PagedSingleListScreen { private final String query; - public ChangeQueryResultsScreen(final String encQuery, - final String positionToken) { + public QueryScreen(final String encQuery, final String positionToken) { super("q," + encQuery, positionToken); query = KeyUtil.decode(encQuery); } @@ -51,7 +64,7 @@ public class ChangeQueryResultsScreen extends PagedSingleListScreen { } else { Gerrit.setQueryString(query); display(result); - ChangeQueryResultsScreen.this.display(); + QueryScreen.this.display(); } } } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLink.java index 8ed7bf1486..2c18448583 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLink.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLink.java @@ -15,9 +15,7 @@ package com.google.gerrit.client.ui; import com.google.gerrit.client.Gerrit; -import com.google.gerrit.client.changes.ByProjectAbandonedChangesScreen; -import com.google.gerrit.client.changes.ByProjectMergedChangesScreen; -import com.google.gerrit.client.changes.ByProjectOpenChangesScreen; +import com.google.gerrit.client.changes.QueryScreen; import com.google.gerrit.common.PageLinks; import com.google.gerrit.reviewdb.Change; import com.google.gerrit.reviewdb.Project; @@ -47,15 +45,18 @@ public class ProjectLink extends InlineHyperlink { private Screen createScreen() { switch (status) { case ABANDONED: - return new ByProjectAbandonedChangesScreen(project, "n,z"); + return QueryScreen.forQuery("status:abandoned " + + QueryScreen.op("project", project.get())); case MERGED: - return new ByProjectMergedChangesScreen(project, "n,z"); + return QueryScreen.forQuery("status:merged " + + QueryScreen.op("project", project.get())); case NEW: case SUBMITTED: default: - return new ByProjectOpenChangesScreen(project, "n,z"); + return QueryScreen.forQuery("status:open " + + QueryScreen.op("project", project.get())); } } } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java index 618c7e0dc4..a52b4ba655 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java @@ -62,7 +62,12 @@ public abstract class Screen extends View { protected void setPageTitle(final String text) { final String old = headerText.getText(); - headerText.setText(text); + if (text.isEmpty()) { + header.setVisible(false); + } else { + headerText.setText(text); + header.setVisible(true); + } if (windowTitle == null || windowTitle == old) { setWindowTitle(text); } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java index b428c9a0ad..120de30573 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java @@ -15,6 +15,7 @@ package com.google.gerrit.httpd.rpc; import com.google.gerrit.common.errors.CorruptEntityException; +import com.google.gerrit.common.errors.InvalidQueryException; import com.google.gerrit.common.errors.NoSuchEntityException; import com.google.gerrit.reviewdb.Account; import com.google.gerrit.reviewdb.ReviewDb; @@ -64,6 +65,8 @@ public class BaseServiceImplementation { if (r != null) { callback.onSuccess(r); } + } catch (InvalidQueryException e) { + callback.onFailure(e); } catch (NoSuchProjectException e) { callback.onFailure(new NoSuchEntityException()); } catch (NoSuchGroupException e) { @@ -116,8 +119,9 @@ public class BaseServiceImplementation { * {@link AsyncCallback#onFailure(Throwable)}. * @throws NoSuchProjectException * @throws NoSuchGroupException + * @throws InvalidQueryException */ T run(ReviewDb db) throws OrmException, Failure, NoSuchProjectException, - NoSuchGroupException; + NoSuchGroupException, InvalidQueryException; } } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java index 56f5a0ff04..9905397915 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java @@ -14,31 +14,29 @@ package com.google.gerrit.httpd.rpc; -import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME; - import com.google.gerrit.common.data.AccountDashboardInfo; import com.google.gerrit.common.data.ChangeInfo; import com.google.gerrit.common.data.ChangeListService; import com.google.gerrit.common.data.SingleListChangeInfo; import com.google.gerrit.common.data.ToggleStarRequest; +import com.google.gerrit.common.errors.InvalidQueryException; import com.google.gerrit.common.errors.NoSuchEntityException; import com.google.gerrit.reviewdb.Account; -import com.google.gerrit.reviewdb.AccountExternalId; import com.google.gerrit.reviewdb.Change; import com.google.gerrit.reviewdb.ChangeAccess; -import com.google.gerrit.reviewdb.PatchLineComment; -import com.google.gerrit.reviewdb.PatchSet; import com.google.gerrit.reviewdb.PatchSetApproval; -import com.google.gerrit.reviewdb.Project; -import com.google.gerrit.reviewdb.RevId; import com.google.gerrit.reviewdb.ReviewDb; import com.google.gerrit.reviewdb.StarredChange; -import com.google.gerrit.reviewdb.TrackingId; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.account.AccountInfoCacheFactory; -import com.google.gerrit.server.config.WildProjectName; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchChangeException; +import com.google.gerrit.server.query.Predicate; +import com.google.gerrit.server.query.QueryParseException; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gerrit.server.query.change.ChangeDataSource; +import com.google.gerrit.server.query.change.ChangeQueryBuilder; +import com.google.gerrit.server.query.change.ChangeQueryRewriter; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwtjsonrpc.client.VoidResult; import com.google.gwtorm.client.OrmException; @@ -90,19 +88,23 @@ public class ChangeListServiceImpl extends BaseServiceImplementation implements private final Provider currentUser; private final ChangeControl.Factory changeControlFactory; private final AccountInfoCacheFactory.Factory accountInfoCacheFactory; - private final Project.NameKey wildProject; + + private final ChangeQueryBuilder queryBuilder; + private final ChangeQueryRewriter queryRewriter; @Inject ChangeListServiceImpl(final Provider schema, final Provider currentUser, final ChangeControl.Factory changeControlFactory, final AccountInfoCacheFactory.Factory accountInfoCacheFactory, - final @WildProjectName Project.NameKey wildProject) { + final ChangeQueryBuilder queryBuilder, + final ChangeQueryRewriter queryRewriter) { super(schema, currentUser); this.currentUser = currentUser; this.changeControlFactory = changeControlFactory; this.accountInfoCacheFactory = accountInfoCacheFactory; - this.wildProject = wildProject; + this.queryBuilder = queryBuilder; + this.queryRewriter = queryRewriter; } private boolean canRead(final Change c) { @@ -113,144 +115,13 @@ public class ChangeListServiceImpl extends BaseServiceImplementation implements } } - public void allOpenPrev(final String pos, final int pageSize, - final AsyncCallback callback) { - run(callback, new QueryPrev(pageSize, pos) { - @Override - ResultSet query(ReviewDb db, int slim, String sortKey) - throws OrmException { - return db.changes().allOpenPrev(sortKey, slim); - } - }); - } - - public void allOpenNext(final String pos, final int pageSize, - final AsyncCallback callback) { - run(callback, new QueryNext(pageSize, pos) { - @Override - ResultSet query(ReviewDb db, int slim, String sortKey) - throws OrmException { - return db.changes().allOpenNext(sortKey, slim); - } - }); - } - - public void myWatchedOpenPrev(final String pos, final int pageSize, - final AsyncCallback callback) { - run(callback, new QueryPrev(pageSize, pos) { - @Override - ResultSet query(ReviewDb db, int slim, String sortKey) - throws OrmException { - return db.changes().allOpenPrev(sortKey, slim); - } - - @Override - protected boolean accept(Change c) { - return isWatched(c); - } - }); - } - - public void myWatchedOpenNext(final String pos, final int pageSize, - final AsyncCallback callback) { - run(callback, new QueryNext(pageSize, pos) { - @Override - ResultSet query(ReviewDb db, int slim, String sortKey) - throws OrmException { - return db.changes().allOpenNext(sortKey, slim); - } - - @Override - protected boolean accept(Change c) { - return isWatched(c); - } - }); - } - - private boolean isWatched(Change c) { - Set watchedProjects = currentUser.get().getWatchedProjects(); - return watchedProjects.contains(c.getProject()) || watchedProjects.contains(wildProject); - } - - public void byProjectOpenPrev(final Project.NameKey project, - final String pos, final int pageSize, - final AsyncCallback callback) { - run(callback, new QueryPrev(pageSize, pos) { - @Override - ResultSet query(ReviewDb db, int slim, String sortKey) - throws OrmException { - return db.changes().byProjectOpenPrev(project, sortKey, slim); - } - }); - } - - public void byProjectOpenNext(final Project.NameKey project, - final String pos, final int pageSize, - final AsyncCallback callback) { - run(callback, new QueryNext(pageSize, pos) { - @Override - ResultSet query(ReviewDb db, int slim, String sortKey) - throws OrmException { - return db.changes().byProjectOpenNext(project, sortKey, slim); - } - }); - } - - public void byProjectClosedPrev(final Project.NameKey project, - final Change.Status s, final String pos, final int pageSize, - final AsyncCallback callback) { - run(callback, new QueryPrev(pageSize, pos) { - @Override - ResultSet query(ReviewDb db, int slim, String sortKey) - throws OrmException { - return db.changes().byProjectClosedPrev(s.getCode(), project, sortKey, - slim); - } - }); - } - - public void byProjectClosedNext(final Project.NameKey project, - final Change.Status s, final String pos, final int pageSize, - final AsyncCallback callback) { - run(callback, new QueryNext(pageSize, pos) { - @Override - ResultSet query(ReviewDb db, int slim, String sortKey) - throws OrmException { - return db.changes().byProjectClosedNext(s.getCode(), project, sortKey, - slim); - } - }); - } - - public void allClosedPrev(final Change.Status s, final String pos, - final int pageSize, final AsyncCallback callback) { - run(callback, new QueryPrev(pageSize, pos) { - @Override - ResultSet query(ReviewDb db, int lim, String key) - throws OrmException { - return db.changes().allClosedPrev(s.getCode(), key, lim); - } - }); - } - - public void allClosedNext(final Change.Status s, final String pos, - final int pageSize, final AsyncCallback callback) { - run(callback, new QueryNext(pageSize, pos) { - @Override - ResultSet query(ReviewDb db, int lim, String key) - throws OrmException { - return db.changes().allClosedNext(s.getCode(), key, lim); - } - }); - } - @Override public void allQueryPrev(final String query, final String pos, final int pageSize, final AsyncCallback callback) { run(callback, new QueryPrev(pageSize, pos) { @Override ResultSet query(ReviewDb db, int lim, String key) - throws OrmException { + throws OrmException, InvalidQueryException { return searchQuery(db, query, lim, key, QUERY_PREV); } }); @@ -262,88 +133,66 @@ public class ChangeListServiceImpl extends BaseServiceImplementation implements run(callback, new QueryNext(pageSize, pos) { @Override ResultSet query(ReviewDb db, int lim, String key) - throws OrmException { + throws OrmException, InvalidQueryException { return searchQuery(db, query, lim, key, QUERY_NEXT); } }); } + @SuppressWarnings("unchecked") private ResultSet searchQuery(final ReviewDb db, String query, final int limit, final String key, final Comparator cmp) - throws OrmException { - List result = new ArrayList(); - final HashSet want = new HashSet(); - query = query.trim(); + throws OrmException, InvalidQueryException { + try { + final Predicate visibleToMe = + queryBuilder.visibleto(currentUser.get()); - if (query.matches("^[1-9][0-9]*$")) { - want.add(Change.Id.parse(query)); + Predicate q = queryBuilder.parse(query); + q = Predicate.and(q, // + cmp == QUERY_PREV // + ? queryBuilder.sortkey_after(key) // + : queryBuilder.sortkey_before(key), // + queryBuilder.limit(limit), // + visibleToMe // + ); + q = queryRewriter.rewrite(q); + if (q instanceof ChangeDataSource) { + ChangeDataSource ds = (ChangeDataSource) q; + ArrayList r = new ArrayList(); + HashSet want = new HashSet(); + for (ChangeData d : ds.read()) { + if (d.hasChange()) { + // Checking visibleToMe here should be unnecessary, the + // query should have already performed it. But we don't + // want to trust the query rewriter that much yet. + // + if (visibleToMe.match(d)) { + r.add(d.getChange()); + } + } else { + want.add(d.getId()); + } + } - } else if (query.matches("^[iI][0-9a-f]{4,}.*$")) { - if (query.startsWith("i")) { - query = "I" + query.substring(1); - } - final Change.Key a = new Change.Key(query); - final Change.Key b = a.max(); - filterBySortKey(result, db.changes().byKeyRange(a, b), cmp, key); - Collections.sort(result, cmp); - if (limit < result.size()) { - result = result.subList(0, limit); - } + // Here we have to check canRead. Its impossible to + // do that test without the change object, and it being + // missing above means we have to compute it ourselves. + // + if (!want.isEmpty()) { + for (Change c : db.changes().get(want)) { + if (canRead(c)) { + r.add(c); + } + } + } - } else if (query.matches("^([0-9a-fA-F]{4," + RevId.LEN + "})$")) { - final RevId id = new RevId(query); - final ResultSet patches; - if (id.isComplete()) { - patches = db.patchSets().byRevision(id); + Collections.sort(r, cmp); + return new ListResultSet(r); } else { - patches = db.patchSets().byRevisionRange(id, id.max()); - } - for (PatchSet p : patches) { - want.add(p.getId().getParentKey()); - } - } else if (query.contains("owner:")) { - String[] parsedQuery = query.split(":"); - if (parsedQuery.length > 1) { - filterBySortKey(result, changesCreatedBy(db, parsedQuery[1]), cmp, key); - } - } else if (query.contains("reviewer:")) { - String[] parsedQuery = query.split(":"); - if (parsedQuery.length > 1) { - want.addAll(changesReviewedBy(db, parsedQuery[1])); - } - } else if (query.contains("bug:") || query.contains("tr:")) { - String[] parsedQuery = query.split(":"); - if (parsedQuery.length > 1) { - want.addAll(changesReferencingTr(db, parsedQuery[1])); - } - } - - if (result.isEmpty() && want.isEmpty()) { - return new ListResultSet(Collections. emptyList()); - } - - filterBySortKey(result, db.changes().get(want), cmp, key); - Collections.sort(result, cmp); - if (limit < result.size()) { - result = result.subList(0, limit); - } - return new ListResultSet(result); - } - - private static void filterBySortKey(final List dst, - final Iterable src, final Comparator cmp, final String key) { - if (cmp == QUERY_PREV) { - for (Change c : src) { - if (c.getSortKey().compareTo(key) > 0) { - dst.add(c); - } - } - } else /* cmp == QUERY_NEXT */{ - for (Change c : src) { - if (c.getSortKey().compareTo(key) < 0) { - dst.add(c); - } + throw new InvalidQueryException("Not Supported", q.toString()); } + } catch (QueryParseException e) { + throw new InvalidQueryException(e.getMessage(), query); } } @@ -403,45 +252,6 @@ public class ChangeListServiceImpl extends BaseServiceImplementation implements }); } - public void myStarredChanges( - final AsyncCallback callback) { - run(callback, new Action() { - public SingleListChangeInfo run(final ReviewDb db) throws OrmException { - final AccountInfoCacheFactory ac = accountInfoCacheFactory.create(); - final SingleListChangeInfo d = new SingleListChangeInfo(); - final Set starred = currentUser.get().getStarredChanges(); - d.setChanges(filter(db.changes().get(starred), starred, ac)); - Collections.sort(d.getChanges(), new Comparator() { - public int compare(final ChangeInfo o1, final ChangeInfo o2) { - return o1.getLastUpdatedOn().compareTo(o2.getLastUpdatedOn()); - } - }); - d.setAccounts(ac.create()); - return d; - } - }); - } - - public void myDraftChanges(final AsyncCallback callback) { - run(callback, new Action() { - public SingleListChangeInfo run(final ReviewDb db) throws OrmException { - final Account.Id me = getAccountId(); - final AccountInfoCacheFactory ac = accountInfoCacheFactory.create(); - final SingleListChangeInfo d = new SingleListChangeInfo(); - final Set starred = currentUser.get().getStarredChanges(); - final Set drafted = draftedBy(db, me); - d.setChanges(filter(db.changes().get(drafted), starred, ac)); - Collections.sort(d.getChanges(), new Comparator() { - public int compare(final ChangeInfo o1, final ChangeInfo o2) { - return o1.getLastUpdatedOn().compareTo(o2.getLastUpdatedOn()); - } - }); - d.setAccounts(ac.create()); - return d; - } - }); - } - public void toggleStars(final ToggleStarRequest req, final AsyncCallback callback) { run(callback, new Action() { @@ -490,102 +300,6 @@ public class ChangeListServiceImpl extends BaseServiceImplementation implements return r; } - private static Set draftedBy(final ReviewDb db, final Account.Id me) - throws OrmException { - final Set existing = new HashSet(); - if (me != null) { - for (final PatchLineComment sc : db.patchComments().draftByAuthor(me)) { - final Change.Id c = - sc.getKey().getParentKey().getParentKey().getParentKey(); - existing.add(c); - } - } - return existing; - } - - /** - * @return a set of all the account ID's matching the given user name in - * either of the following columns: ssh name, email address, full name - */ - private static Set getAccountSources(final ReviewDb db, - final String userName) throws OrmException { - Set result = new HashSet(); - String a = userName; - String b = userName + "\u9fa5"; - addAll(result, db.accounts().suggestByFullName(a, b, 10)); - for (AccountExternalId extId : db.accountExternalIds().suggestByKey( - new AccountExternalId.Key(SCHEME_USERNAME, a), - new AccountExternalId.Key(SCHEME_USERNAME, b), 10)) { - result.add(extId.getAccountId()); - } - for (AccountExternalId extId : db.accountExternalIds() - .suggestByEmailAddress(a, b, 10)) { - result.add(extId.getAccountId()); - } - return result; - } - - private static void addAll(Set result, ResultSet rs) { - for (Account account : rs) { - result.add(account.getId()); - } - } - - /** - * @return a set of all the changes created by userName. This method tries to - * find userName in 1) the ssh user names, 2) the full names and 3) - * the email addresses. The returned changes are unique and sorted by - * time stamp, newer first. - */ - private List changesCreatedBy(final ReviewDb db, final String userName) - throws OrmException { - final List resultChanges = new ArrayList(); - for (Account.Id account : getAccountSources(db, userName)) { - for (Change change : db.changes().byOwnerOpen(account)) { - resultChanges.add(change); - } - for (Change change : db.changes().byOwnerClosedAll(account)) { - resultChanges.add(change); - } - } - return resultChanges; - } - - /** - * @return a set of all the changes reviewed by userName. This method tries to - * find userName in 1) the ssh user names, 2) the full names and the - * email addresses. The returned changes are unique and sorted by time - * stamp, newer first. - */ - private Set changesReviewedBy(final ReviewDb db, - final String userName) throws OrmException { - final Set resultChanges = new HashSet(); - for (Account.Id account : getAccountSources(db, userName)) { - for (PatchSetApproval a : db.patchSetApprovals().openByUser(account)) { - resultChanges.add(a.getPatchSetId().getParentKey()); - } - for (PatchSetApproval a : db.patchSetApprovals().closedByUserAll(account)) { - resultChanges.add(a.getPatchSetId().getParentKey()); - } - } - return resultChanges; - } - - /** - * @return a set of all the changes referencing tracking id. This method find - * all changes with a reference to the given external tracking id. - * The returned changes are unique and sorted by time stamp, newer first. - */ - private Set changesReferencingTr(final ReviewDb db, - final String trackingId) throws OrmException { - final Set resultChanges = new HashSet(); - for (final TrackingId tr : db.trackingIds().byTrackingId( - new TrackingId.Id(trackingId))) { - resultChanges.add(tr.getChangeId()); - } - return resultChanges; - } - private abstract class QueryNext implements Action { protected final String pos; protected final int limit; @@ -597,30 +311,22 @@ public class ChangeListServiceImpl extends BaseServiceImplementation implements this.slim = limit + 1; } - public SingleListChangeInfo run(final ReviewDb db) throws OrmException { + public SingleListChangeInfo run(final ReviewDb db) throws OrmException, + InvalidQueryException { final AccountInfoCacheFactory ac = accountInfoCacheFactory.create(); final SingleListChangeInfo d = new SingleListChangeInfo(); final Set starred = currentUser.get().getStarredChanges(); - boolean results = true; - String sortKey = pos; final ArrayList list = new ArrayList(); - while (results && list.size() < slim) { - results = false; - final ResultSet rs = query(db, slim, sortKey); - for (final Change c : rs) { - results = true; - if (canRead(c) && accept(c)) { - final ChangeInfo ci = new ChangeInfo(c); - ac.want(ci.getOwner()); - ci.setStarred(starred.contains(ci.getId())); - list.add(ci); - if (list.size() == slim) { - rs.close(); - break; - } - } - sortKey = c.getSortKey(); + final ResultSet rs = query(db, slim, pos); + for (final Change c : rs) { + final ChangeInfo ci = new ChangeInfo(c); + ac.want(ci.getOwner()); + ci.setStarred(starred.contains(ci.getId())); + list.add(ci); + if (list.size() == slim) { + rs.close(); + break; } } @@ -630,10 +336,6 @@ public class ChangeListServiceImpl extends BaseServiceImplementation implements return d; } - protected boolean accept(final Change c) { - return true; - } - boolean finish(final ArrayList list) { final boolean atEnd = list.size() <= limit; if (list.size() == slim) { @@ -643,7 +345,7 @@ public class ChangeListServiceImpl extends BaseServiceImplementation implements } abstract ResultSet query(final ReviewDb db, final int slim, - String sortKey) throws OrmException; + String sortKey) throws OrmException, InvalidQueryException; } private abstract class QueryPrev extends QueryNext { diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java index 6beacde7eb..26785a8b81 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java @@ -25,6 +25,9 @@ public interface PatchLineCommentAccess extends @PrimaryKey("key") PatchLineComment get(PatchLineComment.Key id) throws OrmException; + @Query("WHERE key.patchKey.patchSetId.changeId = ?") + ResultSet byChange(Change.Id id) throws OrmException; + @Query("WHERE key.patchKey = ? AND status = '" + PatchLineComment.STATUS_PUBLISHED + "' ORDER BY lineNbr,writtenOn") ResultSet published(Patch.Key patch) throws OrmException; diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingId.java index bd798a064a..7df7619df3 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingId.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingId.java @@ -127,6 +127,14 @@ public final class TrackingId { return key.changeId; } + public String getTrackingId() { + return key.trackingId.get(); + } + + public String getSystem() { + return key.trackingSystem.get(); + } + @Override public int hashCode() { return key.hashCode(); diff --git a/gerrit-server/.settings/org.eclipse.jdt.core.prefs b/gerrit-server/.settings/org.eclipse.jdt.core.prefs index 04afc7fac5..2f45466d84 100644 --- a/gerrit-server/.settings/org.eclipse.jdt.core.prefs +++ b/gerrit-server/.settings/org.eclipse.jdt.core.prefs @@ -1,14 +1,8 @@ -#Tue May 12 17:44:13 PDT 2009 +#Fri Jul 16 23:39:13 PDT 2010 eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning org.eclipse.jdt.core.compiler.source=1.6 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 @@ -252,6 +246,8 @@ org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_ org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.join_lines_in_comments=true +org.eclipse.jdt.core.formatter.join_wrapped_lines=true org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true diff --git a/gerrit-server/src/main/antlr/com/google/gerrit/server/query/Query.g b/gerrit-server/src/main/antlr/com/google/gerrit/server/query/Query.g index 7842c1b2fe..74b785128e 100644 --- a/gerrit-server/src/main/antlr/com/google/gerrit/server/query/Query.g +++ b/gerrit-server/src/main/antlr/com/google/gerrit/server/query/Query.g @@ -19,13 +19,10 @@ options { } tokens { - FIELD_NAME; - DEFAULT_FIELD; - SINGLE_WORD; - EXACT_PHRASE; AND; OR; NOT; + DEFAULT_FIELD; } @header { @@ -63,6 +60,8 @@ package com.google.gerrit.server.query; final QueryLexer lexer = new QueryLexer(new ANTLRStringStream(value)); lexer.mSINGLE_WORD(); return lexer.nextToken().getType() == QueryParser.EOF; + } catch (QueryParseInternalException e) { + return false; } catch (RecognitionException e) { return false; } @@ -81,6 +80,13 @@ package com.google.gerrit.server.query; package com.google.gerrit.server.query; } @lexer::members { + @Override + public void displayRecognitionError(String[] tokenNames, + RecognitionException e) { + String hdr = getErrorHeader(e); + String msg = getErrorMessage(e, tokenNames); + throw new QueryParser.QueryParseInternalException(hdr + " " + msg); + } } query @@ -110,10 +116,12 @@ conditionAnd2 conditionNot : '-' conditionBase -> ^(NOT conditionBase) | NOT^ conditionBase + | VARIABLE_ASSIGN^ conditionOr ')'! | conditionBase ; conditionBase - : (FIELD_NAME ':') => FIELD_NAME^ ':'! fieldValue + : '('! conditionOr ')'! + | (FIELD_NAME ':') => FIELD_NAME^ ':'! fieldValue | fieldValue -> ^(DEFAULT_FIELD fieldValue) ; @@ -121,7 +129,6 @@ fieldValue : n=FIELD_NAME -> SINGLE_WORD[n] | SINGLE_WORD | EXACT_PHRASE - | '('! conditionOr ')'! ; AND: 'AND' ; @@ -133,7 +140,14 @@ WS ; FIELD_NAME - : ('a'..'z')+ + : ('a'..'z' | '_')+ + ; + +VARIABLE_ASSIGN + : ('A'..'Z') ('A'..'Z' | 'a'..'Z')* '=' '(' { + String s = $text; + setText(s.substring(0, s.length() - 2)); + } ; EXACT_PHRASE @@ -164,7 +178,9 @@ fragment NON_WORD // '/' permit | ':' | ';' - | '<' | '=' | '>' + // '<' permit + // '=' permit + // '>' permit | '?' | '[' | ']' | '{' | '}' diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java index 58b872ebbd..a5c780e85a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java @@ -22,7 +22,6 @@ import com.google.gerrit.reviewdb.Change; import com.google.gerrit.reviewdb.Project; import com.google.gerrit.reviewdb.ReviewDb; import com.google.gerrit.reviewdb.StarredChange; -import com.google.gerrit.reviewdb.Project.NameKey; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.Realm; @@ -32,7 +31,6 @@ import com.google.gwtorm.client.OrmException; import com.google.inject.Inject; import com.google.inject.OutOfScopeException; import com.google.inject.Provider; -import com.google.inject.ProvisionException; import com.google.inject.Singleton; import org.eclipse.jgit.lib.PersonIdent; @@ -77,6 +75,11 @@ public class IdentifiedUser extends CurrentUser { return create(AccessPath.UNKNOWN, null, id); } + public IdentifiedUser create(Provider db, Account.Id id) { + return new IdentifiedUser(AccessPath.UNKNOWN, authConfig, canonicalUrl, + realm, accountCache, null, db, id); + } + public IdentifiedUser create(AccessPath accessPath, Provider remotePeerProvider, Account.Id id) { return new IdentifiedUser(accessPath, authConfig, canonicalUrl, realm, diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java index 5e95c18c2b..88ea4d3ebb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java @@ -14,6 +14,8 @@ package com.google.gerrit.server.query; +import com.google.gwtorm.client.OrmException; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -21,64 +23,90 @@ import java.util.Collections; import java.util.List; /** Requires all predicates to be true. */ -public final class AndPredicate extends Predicate { - private final Predicate[] children; +public class AndPredicate extends Predicate { + private final List> children; + private final int cost; - public AndPredicate(final Predicate... that) { + protected AndPredicate(final Predicate... that) { this(Arrays.asList(that)); } - public AndPredicate(final Collection that) { - final ArrayList tmp = new ArrayList(that.size()); - for (Predicate p : that) { - if (p instanceof AndPredicate) { - tmp.addAll(p.getChildren()); + protected AndPredicate(final Collection> that) { + final ArrayList> t = new ArrayList>(that.size()); + int c = 0; + for (Predicate p : that) { + if (getClass() == p.getClass()) { + for (Predicate gp : p.getChildren()) { + t.add(gp); + c += gp.getCost(); + } } else { - tmp.add(p); + t.add(p); + c += p.getCost(); } } - if (tmp.size() < 2) { + if (t.size() < 2) { throw new IllegalArgumentException("Need at least two predicates"); } - children = new Predicate[tmp.size()]; - tmp.toArray(children); + children = t; + cost = c; } @Override - public List getChildren() { - return Collections.unmodifiableList(Arrays.asList(children)); + public final List> getChildren() { + return Collections.unmodifiableList(children); } @Override - public int getChildCount() { - return children.length; + public final int getChildCount() { + return children.size(); } @Override - public Predicate getChild(final int i) { - return children[i]; + public final Predicate getChild(final int i) { + return children.get(i); + } + + @Override + public Predicate copy(final Collection> children) { + return new AndPredicate(children); + } + + @Override + public boolean match(final T object) throws OrmException { + for (final Predicate c : children) { + if (!c.match(object)) { + return false; + } + } + return true; + } + + @Override + public int getCost() { + return cost; } @Override public int hashCode() { - return children[0].hashCode() * 31 + children[1].hashCode(); + return getChild(0).hashCode() * 31 + getChild(1).hashCode(); } @Override public boolean equals(final Object other) { - return other instanceof AndPredicate - && getChildren().equals(((AndPredicate) other).getChildren()); + return getClass() == other.getClass() + && getChildren().equals(((Predicate) other).getChildren()); } @Override - public String toString() { + public final String toString() { final StringBuilder r = new StringBuilder(); r.append("("); - for (int i = 0; i < children.length; i++) { + for (int i = 0; i < getChildCount(); i++) { if (i != 0) { r.append(" "); } - r.append(children[i]); + r.append(getChild(i)); } r.append(")"); return r.toString(); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/ChangeQueryBuilder.java deleted file mode 100644 index b12b56d8ae..0000000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/ChangeQueryBuilder.java +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (C) 2009 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.query; - -import com.google.gerrit.reviewdb.RevId; -import com.google.inject.Singleton; - -import org.eclipse.jgit.lib.AbbreviatedObjectId; - -/** - * Parses a query string meant to be applied to change objects. - *

- * This class is thread-safe, and may be reused across threads to parse queries. - */ -@Singleton -public class ChangeQueryBuilder extends QueryBuilder { - public static final String FIELD_CHANGE = "change"; - public static final String FIELD_COMMIT = "commit"; - public static final String FIELD_REVIEWER = "reviewer"; - public static final String FIELD_OWNER = "owner"; - - private static final String CHANGE_RE = "^[1-9][0-9]*$"; - private static final String COMMIT_RE = - "^([0-9a-fA-F]{4," + RevId.LEN + "})$"; - - @Operator - public Predicate change(final String value) { - match(value, CHANGE_RE); - return new OperatorPredicate(FIELD_CHANGE, value); - } - - @Operator - public Predicate commit(final String value) { - final AbbreviatedObjectId id = AbbreviatedObjectId.fromString(value); - return new ObjectIdPredicate(FIELD_COMMIT, id); - } - - @Operator - public Predicate owner(final String value) { - return new OperatorPredicate(FIELD_OWNER, value); - } - - @Operator - public Predicate reviewer(final String value) { - return new OperatorPredicate(FIELD_REVIEWER, value); - } - - @Override - protected Predicate defaultField(final String value) - throws QueryParseException { - if (value.matches(CHANGE_RE)) { - return change(value); - - } else if (value.matches(COMMIT_RE)) { - return commit(value); - - } else { - throw error("Unsupported query:" + value); - } - } - - private static void match(String val, String re) { - if (!val.matches(re)) { - throw new IllegalArgumentException("Invalid value :" + val); - } - } -} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java new file mode 100644 index 0000000000..b1806b43b4 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java @@ -0,0 +1,54 @@ +// Copyright (C) 2009 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.query; + +/** Predicate to filter a field by matching integer value. */ +public abstract class IntPredicate extends OperatorPredicate { + private final int value; + + public IntPredicate(final String name, final String value) { + super(name, value); + this.value = Integer.parseInt(value); + } + + public IntPredicate(final String name, final int value) { + super(name, String.valueOf(value)); + this.value = value; + } + + public int intValue() { + return value; + } + + @Override + public int hashCode() { + return getOperator().hashCode() * 31 + value; + } + + @Override + public boolean equals(final Object other) { + if (getClass() == other.getClass()) { + final IntPredicate p = (IntPredicate) other; + return getOperator().equals(p.getOperator()) + && intValue() == p.intValue(); + } + return false; + } + + @Override + public String toString() { + return getOperator() + ":" + getValue(); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java index ddb03a6803..9e651a32e7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java @@ -14,25 +14,57 @@ package com.google.gerrit.server.query; +import com.google.gwtorm.client.OrmException; + +import java.util.Collection; import java.util.Collections; import java.util.List; /** Negates the result of another predicate. */ -public final class NotPredicate extends Predicate { - private final Predicate that; +public class NotPredicate extends Predicate { + private final Predicate that; - public NotPredicate(final Predicate that) { + protected NotPredicate(final Predicate that) { + if (that instanceof NotPredicate) { + throw new IllegalArgumentException("Double negation unsupported"); + } this.that = that; } @Override - public Predicate not() { + public final List> getChildren() { + return Collections.singletonList(that); + } + + @Override + public final int getChildCount() { + return 1; + } + + @Override + public final Predicate getChild(final int i) { + if (i != 0) { + throw new ArrayIndexOutOfBoundsException(i); + } return that; } @Override - public List getChildren() { - return Collections.singletonList(that); + public Predicate copy(final Collection> children) { + if (children.size() != 1) { + throw new IllegalArgumentException("Expected exactly one child"); + } + return new NotPredicate(children.iterator().next()); + } + + @Override + public boolean match(final T object) throws OrmException { + return !that.match(object); + } + + @Override + public int getCost() { + return that.getCost(); } @Override @@ -42,12 +74,12 @@ public final class NotPredicate extends Predicate { @Override public boolean equals(final Object other) { - return other instanceof NotPredicate - && getChildren().equals(((Predicate) other).getChildren()); + return getClass() == other.getClass() + && getChildren().equals(((Predicate) other).getChildren()); } @Override - public String toString() { + public final String toString() { return "-" + that.toString(); } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java index bd9eeeaad7..0d68fea98a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java @@ -14,12 +14,14 @@ package com.google.gerrit.server.query; +import com.google.gerrit.server.query.OperatorPredicate; + import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.ObjectId; /** Predicate for a field of {@link ObjectId}. */ -public final class ObjectIdPredicate extends OperatorPredicate { +public abstract class ObjectIdPredicate extends OperatorPredicate { private final AbbreviatedObjectId id; public ObjectIdPredicate(final String name, final AbbreviatedObjectId id) { @@ -47,7 +49,7 @@ public final class ObjectIdPredicate extends OperatorPredicate { @Override public boolean equals(Object other) { if (other instanceof ObjectIdPredicate) { - final ObjectIdPredicate p = (ObjectIdPredicate) other; + final ObjectIdPredicate p = (ObjectIdPredicate) other; return getOperator().equals(p.getOperator()) && id.equals(p.id); } return false; diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java index fbd6af1225..4c6e203f78 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java @@ -14,9 +14,11 @@ package com.google.gerrit.server.query; +import java.util.Collection; + /** Predicate to filter a field by matching value. */ -public class OperatorPredicate extends Predicate { +public abstract class OperatorPredicate extends Predicate { private final String name; private final String value; @@ -33,6 +35,14 @@ public class OperatorPredicate extends Predicate { return value; } + @Override + public Predicate copy(final Collection> children) { + if (!children.isEmpty()) { + throw new IllegalArgumentException("Expected 0 children"); + } + return this; + } + @Override public int hashCode() { return getOperator().hashCode() * 31 + getValue().hashCode(); @@ -41,7 +51,7 @@ public class OperatorPredicate extends Predicate { @Override public boolean equals(final Object other) { if (getClass() == other.getClass()) { - final OperatorPredicate p = (OperatorPredicate) other; + final OperatorPredicate p = (OperatorPredicate) other; return getOperator().equals(p.getOperator()) && getValue().equals(p.getValue()); } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java index a8b8d2b8d0..08f50f4968 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java @@ -14,6 +14,8 @@ package com.google.gerrit.server.query; +import com.google.gwtorm.client.OrmException; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -21,64 +23,90 @@ import java.util.Collections; import java.util.List; /** Requires one predicate to be true. */ -public final class OrPredicate extends Predicate { - private final Predicate[] children; +public class OrPredicate extends Predicate { + private final List> children; + private final int cost; - public OrPredicate(final Predicate... that) { + protected OrPredicate(final Predicate... that) { this(Arrays.asList(that)); } - public OrPredicate(final Collection that) { - final ArrayList tmp = new ArrayList(that.size()); - for (Predicate p : that) { - if (p instanceof OrPredicate) { - tmp.addAll(p.getChildren()); + protected OrPredicate(final Collection> that) { + final ArrayList> t = new ArrayList>(that.size()); + int c = 0; + for (Predicate p : that) { + if (getClass() == p.getClass()) { + for (Predicate gp : p.getChildren()) { + t.add(gp); + c += gp.getCost(); + } } else { - tmp.add(p); + t.add(p); + c += p.getCost(); } } - if (tmp.size() < 2) { + if (t.size() < 2) { throw new IllegalArgumentException("Need at least two predicates"); } - children = new Predicate[tmp.size()]; - tmp.toArray(children); + children = t; + cost = c; } @Override - public List getChildren() { - return Collections.unmodifiableList(Arrays.asList(children)); + public final List> getChildren() { + return Collections.unmodifiableList(children); } @Override - public int getChildCount() { - return children.length; + public final int getChildCount() { + return children.size(); } @Override - public Predicate getChild(final int i) { - return children[i]; + public final Predicate getChild(final int i) { + return children.get(i); + } + + @Override + public Predicate copy(final Collection> children) { + return new OrPredicate(children); + } + + @Override + public boolean match(final T object) throws OrmException { + for (final Predicate c : children) { + if (c.match(object)) { + return true; + } + } + return false; + } + + @Override + public int getCost() { + return cost; } @Override public int hashCode() { - return children[0].hashCode() * 31 + children[1].hashCode(); + return getChild(0).hashCode() * 31 + getChild(1).hashCode(); } @Override public boolean equals(final Object other) { - return other instanceof OrPredicate - && getChildren().equals(((OrPredicate) other).getChildren()); + return getClass() == other.getClass() + && getChildren().equals(((Predicate) other).getChildren()); } @Override - public String toString() { + public final String toString() { final StringBuilder r = new StringBuilder(); r.append("("); - for (int i = 0; i < children.length; i++) { + for (int i = 0; i < getChildCount(); i++) { if (i != 0) { r.append(" OR "); } - r.append(children[i]); + r.append(getChild(i)); } r.append(")"); return r.toString(); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java index 70da79db8a..2455cbd7dc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java @@ -14,6 +14,8 @@ package com.google.gerrit.server.query; +import com.google.gwtorm.client.OrmException; + import java.util.Collection; import java.util.Collections; import java.util.List; @@ -21,44 +23,60 @@ import java.util.List; /** * An abstract predicate tree for any form of query. *

- * Implementations should be immutable, and therefore also be thread-safe. They - * also should ensure their immutable promise by defensively copying any - * structures which might be modified externally, but were passed into the - * object's constructor. + * Implementations should be immutable, such that the meaning of a predicate + * never changes once constructed. They should ensure their immutable promise by + * defensively copying any structures which might be modified externally, but + * was passed into the object's constructor. + *

+ * However, implementations may retain non-thread-safe caches internally, + * to speed up evaluation operations within the context of one thread's + * evaluation of the predicate. As a result, callers should assume predicates + * are not thread-safe, but that two predicate graphs produce the same results + * given the same inputs if they are {@link #equals(Object)}. *

* Predicates should support deep inspection whenever possible, so that generic * algorithms can be written to operate against them. Predicates which contain * other predicates should override {@link #getChildren()} to return the list of * children nested within the predicate. + * + * @type type of object the predicate can evaluate in memory. */ -public abstract class Predicate { +public abstract class Predicate { /** Combine the passed predicates into a single AND node. */ - public static Predicate and(final Predicate... that) { - return new AndPredicate(that); + public static Predicate and(final Predicate... that) { + return new AndPredicate(that); } /** Combine the passed predicates into a single AND node. */ - public static Predicate and(final Collection that) { - return new AndPredicate(that); + public static Predicate and( + final Collection> that) { + return new AndPredicate(that); } /** Combine the passed predicates into a single OR node. */ - public static Predicate or(final Predicate... that) { - return new OrPredicate(that); + public static Predicate or(final Predicate... that) { + return new OrPredicate(that); } /** Combine the passed predicates into a single OR node. */ - public static Predicate or(final Collection that) { - return new OrPredicate(that); + public static Predicate or( + final Collection> that) { + return new OrPredicate(that); } - /** Invert the passed node; same as {@code that.not()}. */ - public static Predicate not(final Predicate that) { - return that.not(); + /** Invert the passed node. */ + @SuppressWarnings("unchecked") + public static Predicate not(final Predicate that) { + if (that instanceof NotPredicate) { + // Negate of a negate is the original predicate. + // + return that.getChild(0); + } + return new NotPredicate(that); } /** Get the children of this predicate, if any. */ - public List getChildren() { + public List> getChildren() { return Collections.emptyList(); } @@ -68,14 +86,22 @@ public abstract class Predicate { } /** Same as {@code getChildren().get(i)} */ - public Predicate getChild(final int i) { + public Predicate getChild(final int i) { return getChildren().get(i); } - /** Obtain the inverse of this predicate. */ - public Predicate not() { - return new NotPredicate(this); - } + /** Create a copy of this predicate, with new children. */ + public abstract Predicate copy(Collection> children); + + /** + * Does this predicate match this object? + * + * @throws OrmException + */ + public abstract boolean match(T object) throws OrmException; + + /** @return a cost estimate to run this predicate, higher figures cost more. */ + public abstract int getCost(); @Override public abstract int hashCode(); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java index da967f45ce..20c5a560de 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java @@ -24,6 +24,7 @@ import static com.google.gerrit.server.query.QueryParser.FIELD_NAME; import static com.google.gerrit.server.query.QueryParser.NOT; import static com.google.gerrit.server.query.QueryParser.OR; import static com.google.gerrit.server.query.QueryParser.SINGLE_WORD; +import static com.google.gerrit.server.query.QueryParser.VARIABLE_ASSIGN; import org.antlr.runtime.tree.Tree; @@ -34,15 +35,14 @@ import java.lang.annotation.Target; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; /** * Base class to support writing parsers for query languages. *

- * This class is thread-safe, and may be reused across threads to parse queries, - * so implementations of this class should also strive to be thread-safe. - *

* Subclasses may document their supported query operators by declaring public * methods that perform the query conversion into a {@link Predicate}. For * example, to support "is:starred", "is:unread", and nothing else, a subclass @@ -70,33 +70,53 @@ import java.util.Map; *

* Subclasses may also declare a handler for values which appear without * operator by overriding {@link #defaultField(String)}. + * + * @param type of object the predicates can evaluate in memory. */ -public abstract class QueryBuilder { - private final Map opFactories = - new HashMap(); +public abstract class QueryBuilder { + /** + * Defines the operators known by a QueryBuilder. + * + * This class is thread-safe and may be reused or cached. + * + * @param type of object the predicates can evaluate in memory. + * @param type of the query builder subclass. + */ + public static class Definition> { + private final Map> opFactories = + new HashMap>(); - protected QueryBuilder() { - // Guess at the supported operators by scanning methods. - // - Class c = getClass(); - while (c != QueryBuilder.class) { - for (final Method method : c.getDeclaredMethods()) { - if (method.getAnnotation(Operator.class) != null - && Predicate.class.isAssignableFrom(method.getReturnType()) - && method.getParameterTypes().length == 1 - && method.getParameterTypes()[0] == String.class - && (method.getModifiers() & Modifier.ABSTRACT) == 0 - && (method.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC) { - final String name = method.getName().toLowerCase(); - if (!opFactories.containsKey(name)) { - opFactories.put(name, new ReflectionFactory(name, method)); + public Definition(Class clazz) { + // Guess at the supported operators by scanning methods. + // + Class c = clazz; + while (c != QueryBuilder.class) { + for (final Method method : c.getDeclaredMethods()) { + if (method.getAnnotation(Operator.class) != null + && Predicate.class.isAssignableFrom(method.getReturnType()) + && method.getParameterTypes().length == 1 + && method.getParameterTypes()[0] == String.class + && (method.getModifiers() & Modifier.ABSTRACT) == 0 + && (method.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC) { + final String name = method.getName().toLowerCase(); + if (!opFactories.containsKey(name)) { + opFactories.put(name, new ReflectionFactory(name, method)); + } } } + c = c.getSuperclass(); } - c = c.getSuperclass(); } } + @SuppressWarnings("unchecked") + private final Map opFactories; + + @SuppressWarnings("unchecked") + protected QueryBuilder(Definition> def) { + opFactories = (Map) def.opFactories; + } + /** * Parse a user supplied query string into a predicate. * @@ -107,11 +127,11 @@ public abstract class QueryBuilder { * due to an operator not being supported, or due to an invalid value * being passed to a recognized operator. */ - public Predicate parse(final String query) throws QueryParseException { + public Predicate parse(final String query) throws QueryParseException { return toPredicate(QueryParser.parse(query)); } - private Predicate toPredicate(final Tree r) throws QueryParseException, + private Predicate toPredicate(final Tree r) throws QueryParseException, IllegalArgumentException { switch (r.getType()) { case AND: @@ -127,12 +147,26 @@ public abstract class QueryBuilder { case FIELD_NAME: return operator(r.getText(), onlyChildOf(r)); + case VARIABLE_ASSIGN: { + final String var = r.getText(); + final Tree opTree = onlyChildOf(r); + if (opTree.getType() == FIELD_NAME) { + final Tree val = onlyChildOf(opTree); + if (val.getType() == SINGLE_WORD && "*".equals(val.getText())) { + final String op = opTree.getText(); + final WildPatternPredicate pat = new WildPatternPredicate(op); + return new VariablePredicate(var, pat); + } + } + return new VariablePredicate(var, toPredicate(opTree)); + } + default: throw error("Unsupported operator: " + r); } } - private Predicate operator(final String name, final Tree val) + private Predicate operator(final String name, final Tree val) throws QueryParseException { switch (val.getType()) { // Expand multiple values, "foo:(a b c)", as though they were written @@ -140,13 +174,13 @@ public abstract class QueryBuilder { // case AND: case OR: { - final Predicate[] p = new Predicate[val.getChildCount()]; - for (int i = 0; i < p.length; i++) { + List> p = new ArrayList>(val.getChildCount()); + for (int i = 0; i < val.getChildCount(); i++) { final Tree c = val.getChild(i); if (c.getType() != DEFAULT_FIELD) { throw error("Nested operator not expected: " + c); } - p[i] = operator(name, onlyChildOf(c)); + p.add(operator(name, onlyChildOf(c))); } return val.getType() == AND ? and(p) : or(p); } @@ -163,16 +197,17 @@ public abstract class QueryBuilder { } } - private Predicate operator(final String name, final String value) + @SuppressWarnings("unchecked") + private Predicate operator(final String name, final String value) throws QueryParseException { final OperatorFactory f = opFactories.get(name); if (f == null) { throw error("Unsupported operator " + name + ":" + value); } - return f.create(value); + return f.create(this, value); } - private Predicate defaultField(final Tree r) throws QueryParseException { + private Predicate defaultField(final Tree r) throws QueryParseException { switch (r.getType()) { case SINGLE_WORD: case EXACT_PHRASE: @@ -197,14 +232,15 @@ public abstract class QueryBuilder { * @return predicate representing this value. * @throws QueryParseException the parser does not recognize this value. */ - protected Predicate defaultField(final String value) + protected Predicate defaultField(final String value) throws QueryParseException { throw error("Unsupported query:" + value); } - private Predicate[] children(final Tree r) throws QueryParseException, + @SuppressWarnings("unchecked") + private Predicate[] children(final Tree r) throws QueryParseException, IllegalArgumentException { - final Predicate[] p = new Predicate[r.getChildCount()]; + final Predicate[] p = new Predicate[r.getChildCount()]; for (int i = 0; i < p.length; i++) { p[i] = toPredicate(r.getChild(i)); } @@ -227,8 +263,8 @@ public abstract class QueryBuilder { } /** Converts a value string passed to an operator into a {@link Predicate}. */ - protected interface OperatorFactory { - Predicate create(String value) throws QueryParseException; + protected interface OperatorFactory> { + Predicate create(Q builder, String value) throws QueryParseException; } /** Denotes a method which is a query operator. */ @@ -237,7 +273,8 @@ public abstract class QueryBuilder { protected @interface Operator { } - private class ReflectionFactory implements OperatorFactory { + private static class ReflectionFactory> + implements OperatorFactory { private final String name; private final Method method; @@ -246,15 +283,20 @@ public abstract class QueryBuilder { this.method = method; } + @SuppressWarnings("unchecked") @Override - public Predicate create(final String value) throws QueryParseException { + public Predicate create(Q builder, String value) + throws QueryParseException { try { - return (Predicate) method.invoke(QueryBuilder.this, value); + return (Predicate) method.invoke(builder, value); } catch (RuntimeException e) { throw error("Error in operator " + name + ":" + value, e); } catch (IllegalAccessException e) { throw error("Error in operator " + name + ":" + value, e); } catch (InvocationTargetException e) { + if (e.getCause() instanceof QueryParseException) { + throw (QueryParseException) e.getCause(); + } throw error("Error in operator " + name + ":" + value, e.getCause()); } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java new file mode 100644 index 0000000000..c44d3865d2 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java @@ -0,0 +1,499 @@ +// Copyright (C) 2009 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.query; + +import com.google.inject.name.Named; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Rewrites a Predicate tree by applying rewrite rules. + *

+ * Subclasses may document their rewrite rules by declaring public methods with + * {@link Rewrite} annotations, such as: + * + *

+ * @Rewrite("A=(owner:*) B=(status:*)")
+ * public Predicate r1_ownerStatus(@Named("A") OperatorPredicate owner,
+ *     @Named("B") OperatorPredicate status) {
+ * }
+ * 
+ *

+ * Rewrite methods are applied in order by declared name, so naming methods with + * a numeric prefix to ensure a specific ordering (if required) is suggested. + * + * @type type of object the predicate can evaluate in memory. + */ +public abstract class QueryRewriter { + /** + * Defines the rewrite rules known by a QueryRewriter. + * + * This class is thread-safe and may be reused or cached. + * + * @param type of object the predicates can evaluate in memory. + * @param type of the rewriter subclass. + */ + public static class Definition> { + private final List> rewriteRules; + + public Definition(Class clazz, QueryBuilder qb) { + rewriteRules = new ArrayList>(); + + Class c = clazz; + while (c != QueryRewriter.class) { + final Method[] declared = c.getDeclaredMethods(); + Arrays.sort(declared, new Comparator() { + @Override + public int compare(Method o1, Method o2) { + return o1.getName().compareTo(o2.getName()); + } + }); + for (Method m : declared) { + final Rewrite rp = m.getAnnotation(Rewrite.class); + if ((m.getModifiers() & Modifier.ABSTRACT) != Modifier.ABSTRACT + && (m.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC + && rp != null) { + rewriteRules.add(new MethodRewrite(qb, rp.value(), m)); + } + } + c = c.getSuperclass(); + } + } + } + + private final List> rewriteRules; + + protected QueryRewriter(final Definition> def) { + this.rewriteRules = def.rewriteRules; + } + + /** Combine the passed predicates into a single AND node. */ + public Predicate and(Collection> that) { + return Predicate.and(that); + } + + /** Combine the passed predicates into a single AND node. */ + public Predicate and(Predicate... that) { + return and(Arrays.asList(that)); + } + + /** Combine the passed predicates into a single OR node. */ + public Predicate or(Collection> that) { + return Predicate.or(that); + } + + /** Combine the passed predicates into a single OR node. */ + public Predicate or(Predicate... that) { + return or(Arrays.asList(that)); + } + + /** Invert the passed node. */ + public Predicate not(Predicate that) { + return Predicate.not(that); + } + + /** + * Apply rewrites to a graph until it stops changing. + * + * @param in the graph to rewrite. + * @return the rewritten graph. + */ + public Predicate rewrite(Predicate in) { + Predicate old; + do { + old = in; + in = rewriteOne(in); + + if (in.getChildCount() > 0) { + List> n = new ArrayList>(in.getChildCount()); + for (Predicate p : in.getChildren()) { + n.add(rewrite(p)); + } + n = removeDuplicates(n); + if (n.size() == 1 && (isAND(in) || isOR(in))) { + in = n.get(0); + } else { + in = in.copy(n); + } + } + + } while (!old.equals(in)); + return replaceGenericNodes(in); + } + + protected Predicate replaceGenericNodes(final Predicate in) { + if (in.getClass() == NotPredicate.class) { + return not(replaceGenericNodes(in.getChild(0))); + + } else if (in.getClass() == AndPredicate.class) { + List> n = new ArrayList>(in.getChildCount()); + for (Predicate c : in.getChildren()) { + n.add(replaceGenericNodes(c)); + } + return and(n); + + } else if (in.getClass() == OrPredicate.class) { + List> n = new ArrayList>(in.getChildCount()); + for (Predicate c : in.getChildren()) { + n.add(replaceGenericNodes(c)); + } + return or(n); + + } else { + return in; + } + } + + private Predicate rewriteOne(Predicate input) { + Predicate best = null; + for (RewriteRule r : rewriteRules) { + Predicate n = r.rewrite(this, input); + if (n == null) { + continue; + } + + if (best == null || n.getCost() < best.getCost()) { + best = n; + continue; + } + } + return best != null ? best : input; + } + + private static class MatchResult { + private static final MatchResult FAIL = new MatchResult(null); + private static final MatchResult OK = new MatchResult(null); + + @SuppressWarnings("unchecked") + static MatchResult fail() { + return (MatchResult) FAIL; + } + + @SuppressWarnings("unchecked") + static MatchResult ok() { + return (MatchResult) OK; + } + + final Predicate extra; + + MatchResult(Predicate extra) { + this.extra = extra; + } + + boolean success() { + return this != FAIL; + } + } + + private MatchResult match(final Map> outVars, + final Predicate pattern, final Predicate actual) { + if (pattern instanceof VariablePredicate) { + final VariablePredicate v = (VariablePredicate) pattern; + final MatchResult r = match(outVars, v.getChild(0), actual); + if (r.success()) { + Predicate old = outVars.get(v.getName()); + if (old == null) { + outVars.put(v.getName(), actual); + return r; + } else if (old.equals(actual)) { + return r; + } else { + return MatchResult.fail(); + } + } else { + return MatchResult.fail(); + } + } + + final int cnt = pattern.getChildCount(); + if ((isAND(pattern) && isAND(actual)) // + || (isOR(pattern) && isOR(actual)) // + || (isNOT(pattern) && isNOT(actual)) // + ) { + // Order doesn't actually matter here. That does make our logic quite + // a bit more complex as we need to consult each child at most once, + // but in any order. + // + final LinkedList> have = dup(actual); + final LinkedList> extra = new LinkedList>(); + for (final Predicate pat : pattern.getChildren()) { + boolean found = false; + for (final Iterator> i = have.iterator(); i.hasNext();) { + final MatchResult r = match(outVars, pat, i.next()); + if (r.success()) { + found = true; + i.remove(); + if (r.extra != null) { + extra.add(r.extra); + } + break; + } + } + if (!found) { + return MatchResult.fail(); + } + } + have.addAll(extra); + switch (have.size()) { + case 0: + return MatchResult.ok(); + case 1: + if (isNOT(actual)) { + return new MatchResult(actual.copy(have)); + } + return new MatchResult(have.get(0)); + default: + return new MatchResult(actual.copy(have)); + } + + } else if (pattern.equals(actual)) { + return MatchResult.ok(); + + } else if (pattern instanceof WildPatternPredicate + && actual instanceof OperatorPredicate + && ((OperatorPredicate) pattern).getOperator().equals( + ((OperatorPredicate) actual).getOperator())) { + return MatchResult.ok(); + + } else { + return MatchResult.fail(); + } + } + + private static LinkedList> dup(final Predicate actual) { + return new LinkedList>(actual.getChildren()); + } + + /** + * Denotes a method which wants to replace a predicate expression. + *

+ * This annotation must be applied to a public method which returns + * {@link Predicate}. The arguments of the method should {@link Predicate}, or + * any subclass of it. The annotation value is a query language string which + * describes the subtree this rewrite applies to. Method arguments should be + * named with a {@link Named} annotation, and the same names should be used in + * the query. + *

+ * For example: + * + *

+   * @Rewrite("A=(owner:*) B=(status:*)")
+   * public Predicate ownerStatus(@Named("A") OperatorPredicate owner,
+   *     @Named("B") OperatorPredicate status) {
+   * }
+   * 
+ * + * matches an AND Predicate with at least two children, one being an operator + * predicate called "owner" and the other being an operator predicate called + * "status". The variables in the query are matched by name against the + * parameters. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + protected @interface Rewrite { + String value(); + } + + /** Applies a rewrite rule to a Predicate. */ + protected interface RewriteRule { + /** + * Apply a rewrite rule to the Predicate. + * + * @param input the input predicate to be tested, and possibly rewritten. + * @return a rewritten form of the predicate if this rule matches with the + * tree {@code input} and has a rewrite for it; {@code null} if this + * rule does not want this predicate. + */ + Predicate rewrite(QueryRewriter rewriter, Predicate input); + } + + /** Implements the magic behind {@link Rewrite} annotations. */ + private static class MethodRewrite implements RewriteRule { + private final Method method; + private final Predicate pattern; + private final String[] argNames; + private final Class>[] argTypes; + + @SuppressWarnings("unchecked") + MethodRewrite(QueryBuilder queryBuilder, String patternText, Method m) { + method = m; + + Predicate p; + try { + p = queryBuilder.parse(patternText); + } catch (QueryParseException e) { + throw new RuntimeException("Bad @Rewrite(\"" + patternText + "\")" + + " on " + m.toGenericString() + " in " + m.getDeclaringClass() + + ": " + e.getMessage(), e); + } + if (!Predicate.class.isAssignableFrom(m.getReturnType())) { + throw new RuntimeException(m.toGenericString() + " in " + + m.getDeclaringClass() + " must return " + Predicate.class); + } + + pattern = p; + argNames = new String[method.getParameterTypes().length]; + argTypes = new Class[argNames.length]; + for (int i = 0; i < argNames.length; i++) { + Named name = null; + for (Annotation a : method.getParameterAnnotations()[i]) { + if (a instanceof Named) { + name = (Named) a; + break; + } + } + if (name == null) { + throw new RuntimeException("Argument " + (i + 1) + " of " + + m.toGenericString() + " in " + m.getDeclaringClass() + + " has no @Named annotation"); + } + if (!Predicate.class.isAssignableFrom(method.getParameterTypes()[i])) { + throw new RuntimeException("Argument " + (i + 1) + " of " + + m.toGenericString() + " in " + m.getDeclaringClass() + + " must be of type " + Predicate.class); + } + argNames[i] = name.value(); + argTypes[i] = (Class>) method.getParameterTypes()[i]; + } + } + + @SuppressWarnings("unchecked") + @Override + public Predicate rewrite(QueryRewriter rewriter, + final Predicate input) { + final HashMap> args = + new HashMap>(); + final MatchResult res = rewriter.match(args, pattern, input); + if (!res.success()) { + return null; + } + + final Predicate[] argList = new Predicate[argNames.length]; + for (int i = 0; i < argList.length; i++) { + argList[i] = args.get(argNames[i]); + if (argList[i] == null) { + final String a = "@Named(\"" + argNames[i] + "\")"; + throw error(new IllegalStateException("No value bound for " + a)); + } + if (!argTypes[i].isInstance(argList[i])) { + return null; + } + } + + final Predicate rep; + try { + rep = (Predicate) method.invoke(rewriter, (Object[]) argList); + } catch (IllegalArgumentException e) { + throw error(e); + } catch (IllegalAccessException e) { + throw error(e); + } catch (InvocationTargetException e) { + throw error(e.getCause()); + } + + if (rep instanceof RewritePredicate) { + ((RewritePredicate) rep).init(method.getName(), argList); + } + + if (res.extra == null) { + return rep; + } + + Predicate extra = removeDuplicates(res.extra); + Predicate[] newArgs = new Predicate[] {extra, rep}; + return input.copy(Arrays.asList(newArgs)); + } + + private IllegalArgumentException error(Throwable e) { + final String msg = "Cannot apply " + method.getName(); + return new IllegalArgumentException(msg, e); + } + } + + private static Predicate removeDuplicates(Predicate in) { + if (in.getChildCount() > 0) { + List> n = removeDuplicates(in.getChildren()); + if (n.size() == 1 && (isAND(in) || isOR(in))) { + in = n.get(0); + } else { + in = in.copy(n); + } + } + return in; + } + + private static List> removeDuplicates(List> n) { + List> r = new ArrayList>(); + for (Predicate p : n) { + if (!r.contains(p)) { + r.add(p); + } + } + return r; + } + + private static void expand(final List> out, + final List> allOR, final List> tmp, + final List> nonOR) { + if (tmp.size() == allOR.size()) { + final int sz = nonOR.size() + tmp.size(); + final List> newList = new ArrayList>(sz); + newList.addAll(nonOR); + newList.addAll(tmp); + out.add(Predicate.and(newList)); + + } else { + for (final Predicate c : allOR.get(tmp.size()).getChildren()) { + try { + tmp.add(c); + expand(out, allOR, tmp, nonOR); + } finally { + tmp.remove(tmp.size() - 1); + } + } + } + } + + @SuppressWarnings("unchecked") + private static boolean isAND(final Predicate p) { + return p instanceof AndPredicate; + } + + @SuppressWarnings("unchecked") + private static boolean isOR(final Predicate p) { + return p instanceof OrPredicate; + } + + @SuppressWarnings("unchecked") + private static boolean isNOT(final Predicate p) { + return p instanceof NotPredicate; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/RewritePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/RewritePredicate.java new file mode 100644 index 0000000000..c0a00ca991 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/RewritePredicate.java @@ -0,0 +1,68 @@ +// 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.query; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public abstract class RewritePredicate extends Predicate { + private String name = getClass().getName(); + private List> children = Collections.emptyList(); + + void init(String name, Predicate[] args) { + this.name = name; + this.children = Arrays.asList(args); + } + + @Override + public Predicate copy(Collection> children) { + return this; + } + + @Override + public boolean equals(Object other) { + return getClass() == other.getClass() + && children.equals(((RewritePredicate) other).children); + } + + @Override + public int hashCode() { + int h = getClass().hashCode(); + if (!children.isEmpty()) { + h *= 31; + h += children.get(0).hashCode(); + } + return h; + } + + @Override + public final String toString() { + final StringBuilder r = new StringBuilder(); + r.append(name); + if (!children.isEmpty()) { + r.append("("); + for (int i = 0; i < children.size(); i++) { + if (i != 0) { + r.append(" "); + } + r.append(children.get(i)); + } + r.append(")"); + } + return r.toString(); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java new file mode 100644 index 0000000000..0f6f957f46 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java @@ -0,0 +1,96 @@ +// Copyright (C) 2009 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.query; + +import com.google.gwtorm.client.OrmException; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Holds another predicate in a named variable. + * + * @see QueryRewriter + */ +public class VariablePredicate extends Predicate { + private final String name; + private final Predicate that; + + protected VariablePredicate(final String name, final Predicate that) { + this.name = name; + this.that = that; + } + + public String getName() { + return name; + } + + @Override + public final List> getChildren() { + return Collections.singletonList(that); + } + + @Override + public final int getChildCount() { + return 1; + } + + @Override + public final Predicate getChild(final int i) { + if (i != 0) { + throw new ArrayIndexOutOfBoundsException(i); + } + return that; + } + + @Override + public Predicate copy(final Collection> children) { + if (children.size() != 1) { + throw new IllegalArgumentException("Expected exactly one child"); + } + return new VariablePredicate(getName(), children.iterator().next()); + } + + @Override + public boolean match(final T object) throws OrmException { + return that.match(object); + } + + @Override + public int getCost() { + return that.getCost(); + } + + @Override + public int hashCode() { + return getName().hashCode() * 31 + that.hashCode(); + } + + @Override + public boolean equals(final Object other) { + if (getClass() == other.getClass()) { + final VariablePredicate v = (VariablePredicate) other; + return getName().equals(v.getName()) + && getChildren().equals(v.getChildren()); + } + return false; + } + + @Override + public final String toString() { + return getName() + "=(" + that.toString() + ")"; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/WildPatternPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/WildPatternPredicate.java new file mode 100644 index 0000000000..61c8ea928c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/WildPatternPredicate.java @@ -0,0 +1,59 @@ +// Copyright (C) 2009 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.query; + +/** + * Predicate only for use in rewrite rule patterns. + *

+ * May only be used when nested immediately within a + * {@link VariablePredicate}. Within the QueryRewriter this predicate matches + * any other operator whose name matches this predicate's operator name. + * + * @see QueryRewriter + */ +public final class WildPatternPredicate extends OperatorPredicate { + public WildPatternPredicate(final String name) { + super(name, "*"); + } + + @Override + public boolean match(final T object) { + throw new UnsupportedOperationException("Cannot match " + toString()); + } + + @Override + public int getCost() { + return 0; + } + + @Override + public int hashCode() { + return getOperator().hashCode() * 31; + } + + @Override + public boolean equals(final Object other) { + if (getClass() == other.getClass()) { + final WildPatternPredicate p = (WildPatternPredicate) other; + return getOperator().equals(p.getOperator()); + } + return false; + } + + @Override + public String toString() { + return getOperator() + ":" + getValue(); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/MineDraftsScreen.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AbstractResultSet.java similarity index 50% rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/MineDraftsScreen.java rename to gerrit-server/src/main/java/com/google/gerrit/server/query/change/AbstractResultSet.java index 75f42b144d..2b8d37d284 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/MineDraftsScreen.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AbstractResultSet.java @@ -1,4 +1,4 @@ -// Copyright (C) 2008 The Android Open Source Project +// 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. @@ -12,27 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.client.changes; +package com.google.gerrit.server.query.change; -import com.google.gerrit.client.Gerrit; -import com.google.gerrit.common.PageLinks; +import com.google.gwtorm.client.ResultSet; +import java.util.ArrayList; +import java.util.List; -public class MineDraftsScreen extends MineSingleListScreen { - public MineDraftsScreen() { - super(PageLinks.MINE_DRAFTS); - } - +abstract class AbstractResultSet implements ResultSet { @Override - protected void onInitUI() { - super.onInitUI(); - setWindowTitle(Gerrit.C.menuMyDrafts()); - setPageTitle(Util.C.draftsHeading()); - } - - @Override - protected void onLoad() { - super.onLoad(); - Util.LIST_SVC.myDraftChanges(loadCallback()); + public List toList() { + ArrayList r = new ArrayList(); + for (T t : this) { + r.add(t); + } + return r; } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java new file mode 100644 index 0000000000..e74b390db8 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java @@ -0,0 +1,149 @@ +// 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.query.change; + +import com.google.gerrit.server.query.AndPredicate; +import com.google.gerrit.server.query.Predicate; +import com.google.gwtorm.client.OrmException; +import com.google.gwtorm.client.ResultSet; +import com.google.gwtorm.client.impl.ListResultSet; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +class AndSource extends AndPredicate implements ChangeDataSource { + private static final Comparator> CMP = + new Comparator>() { + @Override + public int compare(Predicate a, Predicate b) { + int ai = a instanceof ChangeDataSource ? 0 : 1; + int bi = b instanceof ChangeDataSource ? 0 : 1; + int cmp = ai - bi; + + if (cmp == 0 // + && a instanceof ChangeDataSource // + && b instanceof ChangeDataSource) { + ai = ((ChangeDataSource) a).hasChange() ? 0 : 1; + bi = ((ChangeDataSource) b).hasChange() ? 0 : 1; + cmp = ai - bi; + } + + if (cmp == 0) { + cmp = a.getCost() - b.getCost(); + } + + if (cmp == 0 // + && a instanceof ChangeDataSource // + && b instanceof ChangeDataSource) { + ChangeDataSource as = (ChangeDataSource) a; + ChangeDataSource bs = (ChangeDataSource) b; + cmp = as.getCardinality() - bs.getCardinality(); + } + + return cmp; + } + }; + + private static List> sort( + Collection> that) { + ArrayList> r = + new ArrayList>(that); + Collections.sort(r, CMP); + return r; + } + + private int cardinality = -1; + + AndSource(final Collection> that) { + super(sort(that)); + } + + @Override + public boolean hasChange() { + ChangeDataSource source = source(); + return source != null && source.hasChange(); + } + + @Override + public ResultSet read() throws OrmException { + ChangeDataSource source = source(); + if (source == null) { + throw new OrmException("No ChangeDataSource: " + this); + } + + // TODO(spearce) This probably should be more lazy. + // + ArrayList r = new ArrayList(); + ChangeData last = null; + boolean skipped = false; + for (ChangeData data : source.read()) { + if (match(data)) { + r.add(data); + } else { + skipped = true; + } + last = data; + } + + if (skipped && last != null && source instanceof Paginated) { + // If we our source is a paginated source and we skipped at + // least one of its results, we may not have filled the full + // limit the caller wants. Restart the source and continue. + // + Paginated p = (Paginated) source; + while (skipped && r.size() < p.limit()) { + ChangeData lastBeforeRestart = last; + skipped = false; + last = null; + for (ChangeData data : p.restart(lastBeforeRestart)) { + if (match(data)) { + r.add(data); + } else { + skipped = true; + } + last = data; + } + } + } + + return new ListResultSet(r); + } + + private ChangeDataSource source() { + for (Predicate p : getChildren()) { + if (p instanceof ChangeDataSource) { + return (ChangeDataSource) p; + } + } + return null; + } + + @Override + public int getCardinality() { + if (cardinality < 0) { + cardinality = Integer.MAX_VALUE; + for (Predicate p : getChildren()) { + if (p instanceof ChangeDataSource) { + int c = ((ChangeDataSource) p).getCardinality(); + cardinality = Math.min(cardinality, c); + } + } + } + return cardinality; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java new file mode 100644 index 0000000000..ae48fdc6f9 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java @@ -0,0 +1,47 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.Branch; +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +class BranchPredicate extends OperatorPredicate { + private final Provider dbProvider; + private final String shortName; + + BranchPredicate(Provider dbProvider, String branch) { + super(ChangeQueryBuilder.FIELD_BRANCH, branch); + this.dbProvider = dbProvider; + this.shortName = new Branch.NameKey(null, branch).getShortName(); + } + + @Override + public boolean match(final ChangeData object) throws OrmException { + Change change = object.change(dbProvider); + if (change == null) { + return false; + } + return shortName.equals(change.getDest().getShortName()); + } + + @Override + public int getCost() { + return 1; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeCosts.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeCosts.java new file mode 100644 index 0000000000..ba42803cc7 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeCosts.java @@ -0,0 +1,39 @@ +// 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.query.change; + +public class ChangeCosts { + public static final int IDS_MEMORY = 1; + public static final int CHANGES_SCAN = 2; + public static final int TR_SCAN = 20; + public static final int APPROVALS_SCAN = 30; + public static final int PATCH_SETS_SCAN = 30; + + /** Estimated matches for a Change-Id string. */ + public static final int CARD_KEY = 5; + + /** Estimated matches for a commit SHA-1 string. */ + public static final int CARD_COMMIT = 5; + + /** Estimated matches for a tracking/bug id string. */ + public static final int CARD_TRACKING_IDS = 5; + + public static int cost(int cost, int cardinality) { + return Math.max(1, cost) * Math.max(0, cardinality); + } + + private ChangeCosts() { + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java new file mode 100644 index 0000000000..d7bffc42b2 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java @@ -0,0 +1,105 @@ +// Copyright (C) 2009 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.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.PatchLineComment; +import com.google.gerrit.reviewdb.PatchSet; +import com.google.gerrit.reviewdb.PatchSetApproval; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.reviewdb.TrackingId; +import com.google.gerrit.server.CurrentUser; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +import java.util.Collection; + +public class ChangeData { + private final Change.Id legacyId; + private Change change; + private Collection patches; + private Collection approvals; + private Collection comments; + private Collection trackingIds; + private CurrentUser visibleTo; + + public ChangeData(final Change.Id id) { + legacyId = id; + } + + public ChangeData(final Change c) { + legacyId = c.getId(); + change = c; + } + + public Change.Id getId() { + return legacyId; + } + + public Change getChange() { + return change; + } + + public boolean hasChange() { + return change != null; + } + + boolean fastIsVisibleTo(CurrentUser user) { + return visibleTo == user; + } + + void cacheVisibleTo(CurrentUser user) { + visibleTo = user; + } + + public Change change(Provider db) throws OrmException { + if (change == null) { + change = db.get().changes().get(legacyId); + } + return change; + } + + public Collection patches(Provider db) + throws OrmException { + if (patches == null) { + patches = db.get().patchSets().byChange(legacyId).toList(); + } + return patches; + } + + public Collection approvals(Provider db) + throws OrmException { + if (approvals == null) { + approvals = db.get().patchSetApprovals().byChange(legacyId).toList(); + } + return approvals; + } + + public Collection comments(Provider db) + throws OrmException { + if (comments == null) { + comments = db.get().patchComments().byChange(legacyId).toList(); + } + return comments; + } + + public Collection trackingIds(Provider db) + throws OrmException { + if (trackingIds == null) { + trackingIds = db.get().trackingIds().byChange(legacyId).toList(); + } + return trackingIds; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataResultSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataResultSet.java new file mode 100644 index 0000000000..fc7ba5922a --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataResultSet.java @@ -0,0 +1,130 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.PatchSet; +import com.google.gerrit.reviewdb.PatchSetApproval; +import com.google.gwtorm.client.ResultSet; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.NoSuchElementException; + +abstract class ChangeDataResultSet extends AbstractResultSet { + static ResultSet change(final ResultSet rs) { + return new ChangeDataResultSet(rs, true) { + @Override + ChangeData convert(Change t) { + return new ChangeData(t); + } + }; + } + + static ResultSet patchSet(final ResultSet rs) { + return new ChangeDataResultSet(rs, false) { + @Override + ChangeData convert(PatchSet t) { + return new ChangeData(t.getId().getParentKey()); + } + }; + } + + static ResultSet patchSetApproval( + final ResultSet rs) { + return new ChangeDataResultSet(rs, false) { + @Override + ChangeData convert(PatchSetApproval t) { + return new ChangeData(t.getPatchSetId().getParentKey()); + } + }; + } + + private final ResultSet source; + private final boolean unique; + + ChangeDataResultSet(ResultSet source, boolean unique) { + this.source = source; + this.unique = unique; + } + + @Override + public Iterator iterator() { + if (unique) { + return new Iterator() { + private final Iterator itr = source.iterator(); + + @Override + public boolean hasNext() { + return itr.hasNext(); + } + + @Override + public ChangeData next() { + return convert(itr.next()); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + + } else { + return new Iterator() { + private final Iterator itr = source.iterator(); + private final HashSet seen = new HashSet(); + private ChangeData next; + + @Override + public boolean hasNext() { + if (next != null) { + return true; + } + while (itr.hasNext()) { + ChangeData d = convert(itr.next()); + if (seen.add(d.getId())) { + next = d; + return true; + } + } + return false; + } + + @Override + public ChangeData next() { + if (hasNext()) { + ChangeData r = next; + next = null; + return r; + } + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + } + + @Override + public void close() { + source.close(); + } + + abstract ChangeData convert(T t); +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java new file mode 100644 index 0000000000..a770b5443d --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java @@ -0,0 +1,29 @@ +// 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.query.change; + +import com.google.gwtorm.client.OrmException; +import com.google.gwtorm.client.ResultSet; + +public interface ChangeDataSource { + /** @return an estimate of the number of results from {@link #read()}. */ + public int getCardinality(); + + /** @return true if all returned ChangeData.hasChange() will be true. */ + public boolean hasChange(); + + /** @return read from the database and return the changes. */ + public abstract ResultSet read() throws OrmException; +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java new file mode 100644 index 0000000000..83107bb329 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java @@ -0,0 +1,69 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.gwtorm.client.ResultSet; +import com.google.inject.Provider; + +class ChangeIdPredicate extends OperatorPredicate implements + ChangeDataSource { + private final Provider dbProvider; + + ChangeIdPredicate(Provider dbProvider, String id) { + super(ChangeQueryBuilder.FIELD_CHANGE, id); + this.dbProvider = dbProvider; + } + + @Override + public boolean match(final ChangeData cd) throws OrmException { + Change change = cd.change(dbProvider); + if (change == null) { + return false; + } + + String key = change.getKey().get(); + if (key.equals(getValue()) || key.startsWith(getValue())) { + return true; + } + return false; + } + + @Override + public ResultSet read() throws OrmException { + Change.Key a = new Change.Key(getValue()); + Change.Key b = a.max(); + return ChangeDataResultSet.change( // + dbProvider.get().changes().byKeyRange(a, b)); + } + + @Override + public boolean hasChange() { + return true; + } + + @Override + public int getCost() { + return ChangeCosts.cost(ChangeCosts.CHANGES_SCAN, getCardinality()); + } + + @Override + public int getCardinality() { + return ChangeCosts.CARD_KEY; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java new file mode 100644 index 0000000000..43828f0833 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java @@ -0,0 +1,375 @@ +// Copyright (C) 2009 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.query.change; + +import com.google.gerrit.common.data.ApprovalTypes; +import com.google.gerrit.reviewdb.Account; +import com.google.gerrit.reviewdb.AccountGroup; +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.Project; +import com.google.gerrit.reviewdb.RevId; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.AccountResolver; +import com.google.gerrit.server.account.GroupCache; +import com.google.gerrit.server.config.AuthConfig; +import com.google.gerrit.server.config.WildProjectName; +import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.query.IntPredicate; +import com.google.gerrit.server.query.Predicate; +import com.google.gerrit.server.query.QueryBuilder; +import com.google.gerrit.server.query.QueryParseException; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; + +import org.eclipse.jgit.lib.AbbreviatedObjectId; + +import java.util.Collection; +import java.util.HashSet; +import java.util.regex.Pattern; + +/** + * Parses a query string meant to be applied to change objects. + */ +public class ChangeQueryBuilder extends QueryBuilder { + private static final Pattern PAT_LEGACY_ID = Pattern.compile("^[1-9][0-9]*$"); + private static final Pattern PAT_CHANGE_ID = + Pattern.compile("^[iI][0-9a-f]{4,}.*$"); + private static final Pattern DEF_CHANGE = + Pattern.compile("^([1-9][0-9]*|[iI][0-9a-f]{4,}.*)$"); + + private static final Pattern PAT_COMMIT = + Pattern.compile("^([0-9a-fA-F]{4," + RevId.LEN + "})$"); + private static final Pattern PAT_EMAIL = + Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+"); + + private static final Pattern PAT_LABEL = + Pattern.compile("^[a-zA-Z][a-zA-Z0-9]*((=|>=|<=)[+-]?|[+-])\\d+$"); + + public static final String FIELD_BRANCH = "branch"; + public static final String FIELD_CHANGE = "change"; + public static final String FIELD_COMMIT = "commit"; + public static final String FIELD_DRAFTBY = "draftby"; + public static final String FIELD_IS = "is"; + public static final String FIELD_HAS = "has"; + public static final String FIELD_LABEL = "label"; + public static final String FIELD_OWNER = "owner"; + public static final String FIELD_PROJECT = "project"; + public static final String FIELD_REF = "ref"; + public static final String FIELD_REVIEWER = "reviewer"; + public static final String FIELD_STARREDBY = "starredby"; + public static final String FIELD_STATUS = "status"; + public static final String FIELD_TOPIC = "topic"; + public static final String FIELD_TR = "tr"; + public static final String FIELD_VISIBLETO = "visibleto"; + public static final String FIELD_WATCHEDBY = "watchedby"; + + private static final QueryBuilder.Definition mydef = + new QueryBuilder.Definition( + ChangeQueryBuilder.class); + + private final Provider dbProvider; + private final Provider currentUser; + private final IdentifiedUser.GenericFactory userFactory; + private final ChangeControl.Factory changeControlFactory; + private final AccountResolver accountResolver; + private final GroupCache groupCache; + private final AuthConfig authConfig; + private final ApprovalTypes approvalTypes; + private final Project.NameKey wildProjectName; + + @Inject + ChangeQueryBuilder(Provider dbProvider, + Provider currentUser, + IdentifiedUser.GenericFactory userFactory, + ChangeControl.Factory changeControlFactory, + AccountResolver accountResolver, GroupCache groupCache, + AuthConfig authConfig, ApprovalTypes approvalTypes, + @WildProjectName Project.NameKey wildProjectName) { + super(mydef); + this.dbProvider = dbProvider; + this.currentUser = currentUser; + this.userFactory = userFactory; + this.changeControlFactory = changeControlFactory; + this.accountResolver = accountResolver; + this.groupCache = groupCache; + this.authConfig = authConfig; + this.approvalTypes = approvalTypes; + this.wildProjectName = wildProjectName; + } + + Provider getReviewDbProvider() { + return dbProvider; + } + + @Operator + public Predicate change(String query) { + if (PAT_LEGACY_ID.matcher(query).matches()) { + return new LegacyChangeIdPredicate(dbProvider, Change.Id.parse(query)); + + } else if (PAT_CHANGE_ID.matcher(query).matches()) { + if (query.charAt(0) == 'i') { + query = "I" + query.substring(1); + } + return new ChangeIdPredicate(dbProvider, query); + } + + throw new IllegalArgumentException(); + } + + @Operator + public Predicate status(String statusName) { + if ("open".equals(statusName)) { + return ChangeStatusPredicate.open(dbProvider); + + } else if ("closed".equals(statusName)) { + return ChangeStatusPredicate.closed(dbProvider); + + } else if ("reviewed".equalsIgnoreCase(statusName)) { + return new IsReviewedPredicate(dbProvider); + + } else { + return new ChangeStatusPredicate(dbProvider, statusName); + } + } + + @Operator + public Predicate has(String value) { + if ("star".equalsIgnoreCase(value)) { + return new IsStarredByPredicate(dbProvider, currentUser.get()); + } + + if ("draft".equalsIgnoreCase(value)) { + if (currentUser.get() instanceof IdentifiedUser) { + return new HasDraftByPredicate(dbProvider, + ((IdentifiedUser) currentUser.get()).getAccountId()); + } + } + + throw new IllegalArgumentException(); + } + + @Operator + public Predicate is(String value) { + if ("starred".equalsIgnoreCase(value)) { + return new IsStarredByPredicate(dbProvider, currentUser.get()); + } + + if ("watched".equalsIgnoreCase(value)) { + return new IsWatchedByPredicate(dbProvider, wildProjectName, // + currentUser.get()); + } + + if ("visible".equalsIgnoreCase(value)) { + return new IsVisibleToPredicate(dbProvider, changeControlFactory, + currentUser.get()); + } + + if ("reviewed".equalsIgnoreCase(value)) { + return new IsReviewedPredicate(dbProvider); + } + + try { + return status(value); + } catch (IllegalArgumentException e) { + // not status: alias? + } + + throw new IllegalArgumentException(); + } + + @Operator + public Predicate commit(String id) { + return new CommitPredicate(dbProvider, AbbreviatedObjectId.fromString(id)); + } + + @Operator + public Predicate project(String name) { + return new ProjectPredicate(dbProvider, name); + } + + @Operator + public Predicate branch(String name) { + return new BranchPredicate(dbProvider, name); + } + + @Operator + public Predicate topic(String name) { + return new TopicPredicate(dbProvider, name); + } + + @Operator + public Predicate ref(String ref) { + return new RefPredicate(dbProvider, ref); + } + + @Operator + public Predicate label(String name) { + return new LabelPredicate(dbProvider, approvalTypes, name); + } + + @Operator + public Predicate starredby(String who) + throws QueryParseException, OrmException { + Account account = accountResolver.find(who); + if (account == null) { + throw error("User " + who + " not found"); + } + return new IsStarredByPredicate(dbProvider, // + userFactory.create(dbProvider, account.getId())); + } + + @Operator + public Predicate watchedby(String who) + throws QueryParseException, OrmException { + Account account = accountResolver.find(who); + if (account == null) { + throw error("User " + who + " not found"); + } + return new IsWatchedByPredicate(dbProvider, wildProjectName, // + userFactory.create(dbProvider, account.getId())); + } + + @Operator + public Predicate draftby(String who) throws QueryParseException, + OrmException { + Account account = accountResolver.find(who); + if (account == null) { + throw error("User " + who + " not found"); + } + return new HasDraftByPredicate(dbProvider, account.getId()); + } + + @Operator + public Predicate visibleto(String who) + throws QueryParseException, OrmException { + Account account = accountResolver.find(who); + if (account != null) { + return visibleto(userFactory.create(dbProvider, account.getId())); + } + + // If its not an account, maybe its a group? + // + AccountGroup g = groupCache.get(new AccountGroup.NameKey(who)); + if (g != null) { + return visibleto(new SingleGroupUser(authConfig, g.getId())); + } + + Collection matches = + groupCache.get(new AccountGroup.ExternalNameKey(who)); + if (matches != null && !matches.isEmpty()) { + HashSet ids = new HashSet(); + for (AccountGroup group : matches) { + ids.add(group.getId()); + } + return visibleto(new SingleGroupUser(authConfig, ids)); + } + + throw error("No user or group matches \"" + who + "\"."); + } + + public Predicate visibleto(CurrentUser user) { + return new IsVisibleToPredicate(dbProvider, changeControlFactory, user); + } + + @Operator + public Predicate owner(String who) throws QueryParseException, + OrmException { + Account account = accountResolver.find(who); + if (account == null) { + throw error("User " + who + " not found"); + } + return new OwnerPredicate(dbProvider, account.getId()); + } + + @Operator + public Predicate reviewer(String nameOrEmail) + throws QueryParseException, OrmException { + Account account = accountResolver.find(nameOrEmail); + if (account == null) { + throw error("Reviewer " + nameOrEmail + " not found"); + } + return new ReviewerPredicate(dbProvider, account.getId()); + } + + @Operator + public Predicate tr(String trackingId) { + return new TrackingIdPredicate(dbProvider, trackingId); + } + + @Operator + public Predicate bug(String trackingId) { + return tr(trackingId); + } + + @Operator + public Predicate limit(String limit) { + return limit(Integer.parseInt(limit)); + } + + public Predicate limit(int limit) { + return new IntPredicate("limit", limit) { + @Override + public boolean match(ChangeData object) { + return true; + } + + @Override + public int getCost() { + return 0; + } + }; + } + + @Operator + public Predicate sortkey_after(String sortKey) { + return new SortKeyPredicate.After(dbProvider, sortKey); + } + + @Operator + public Predicate sortkey_before(String sortKey) { + return new SortKeyPredicate.Before(dbProvider, sortKey); + } + + @SuppressWarnings("unchecked") + @Override + protected Predicate defaultField(String query) + throws QueryParseException { + if (query.startsWith("refs/")) { + return ref(query); + + } else if (DEF_CHANGE.matcher(query).matches()) { + return change(query); + + } else if (PAT_COMMIT.matcher(query).matches()) { + return commit(query); + + } else if (PAT_EMAIL.matcher(query).find()) { + try { + return Predicate.or(owner(query), reviewer(query)); + } catch (OrmException err) { + throw error("Cannot lookup user", err); + } + + } else if (PAT_LABEL.matcher(query).find()) { + return label(query); + + } else { + throw error("Unsupported query:" + query); + } + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java new file mode 100644 index 0000000000..5646e88395 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java @@ -0,0 +1,561 @@ +// Copyright (C) 2009 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.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.ChangeAccess; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.query.IntPredicate; +import com.google.gerrit.server.query.Predicate; +import com.google.gerrit.server.query.QueryRewriter; +import com.google.gerrit.server.query.RewritePredicate; +import com.google.gwtorm.client.OrmException; +import com.google.gwtorm.client.ResultSet; +import com.google.inject.Inject; +import com.google.inject.OutOfScopeException; +import com.google.inject.Provider; +import com.google.inject.name.Named; + +import java.util.Collection; + +public class ChangeQueryRewriter extends QueryRewriter { + private static final QueryRewriter.Definition mydef = + new QueryRewriter.Definition( + ChangeQueryRewriter.class, new ChangeQueryBuilder( + new InvalidProvider(), + new InvalidProvider(), // + null, null, null, null, null, null, null)); + + private final Provider dbProvider; + + @Inject + ChangeQueryRewriter(Provider dbProvider) { + super(mydef); + this.dbProvider = dbProvider; + } + + @Override + public Predicate and(Collection> l) { + return hasSource(l) ? new AndSource(l) : super.and(l); + } + + @Override + public Predicate or(Collection> l) { + return hasSource(l) ? new OrSource(l) : super.or(l); + } + + @Rewrite("-status:open") + public Predicate r00_notOpen() { + return ChangeStatusPredicate.closed(dbProvider); + } + + @Rewrite("-status:closed") + public Predicate r00_notClosed() { + return ChangeStatusPredicate.open(dbProvider); + } + + @SuppressWarnings("unchecked") + @Rewrite("-status:merged") + public Predicate r00_notMerged() { + return or(ChangeStatusPredicate.open(dbProvider), + new ChangeStatusPredicate(dbProvider, Change.Status.ABANDONED)); + } + + @SuppressWarnings("unchecked") + @Rewrite("-status:abandoned") + public Predicate r00_notAbandoned() { + return or(ChangeStatusPredicate.open(dbProvider), + new ChangeStatusPredicate(dbProvider, Change.Status.MERGED)); + } + + @Rewrite("status:open P=(project:*) S=(sortkey_after:*) L=(limit:*)") + public Predicate r10_byProjectOpenPrev( + @Named("P") final ProjectPredicate p, + @Named("S") final SortKeyPredicate.After s, + @Named("L") final IntPredicate l) { + return new PaginatedSource(500, s.getValue(), l.intValue()) { + @Override + ResultSet scan(ChangeAccess a, String key, int limit) + throws OrmException { + return a.byProjectOpenPrev(p.getValueKey(), key, limit); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus().isOpen() // + && p.match(cd) // + && s.match(cd); + } + }; + } + + @Rewrite("status:open P=(project:*) S=(sortkey_before:*) L=(limit:*)") + public Predicate r10_byProjectOpenNext( + @Named("P") final ProjectPredicate p, + @Named("S") final SortKeyPredicate.Before s, + @Named("L") final IntPredicate l) { + return new PaginatedSource(500, s.getValue(), l.intValue()) { + @Override + ResultSet scan(ChangeAccess a, String key, int limit) + throws OrmException { + return a.byProjectOpenNext(p.getValueKey(), key, limit); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus().isOpen() // + && p.match(cd) // + && s.match(cd); + } + }; + } + + @Rewrite("status:merged P=(project:*) S=(sortkey_after:*) L=(limit:*)") + public Predicate r10_byProjectMergedPrev( + @Named("P") final ProjectPredicate p, + @Named("S") final SortKeyPredicate.After s, + @Named("L") final IntPredicate l) { + return new PaginatedSource(40000, s.getValue(), l.intValue()) { + @Override + ResultSet scan(ChangeAccess a, String key, int limit) + throws OrmException { + return a.byProjectClosedPrev(Change.Status.MERGED.getCode(), // + p.getValueKey(), key, limit); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus() == Change.Status.MERGED + && p.match(cd) // + && s.match(cd); + } + }; + } + + @Rewrite("status:merged P=(project:*) S=(sortkey_before:*) L=(limit:*)") + public Predicate r10_byProjectMergedNext( + @Named("P") final ProjectPredicate p, + @Named("S") final SortKeyPredicate.Before s, + @Named("L") final IntPredicate l) { + return new PaginatedSource(40000, s.getValue(), l.intValue()) { + @Override + ResultSet scan(ChangeAccess a, String key, int limit) + throws OrmException { + return a.byProjectClosedNext(Change.Status.MERGED.getCode(), // + p.getValueKey(), key, limit); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus() == Change.Status.MERGED + && p.match(cd) // + && s.match(cd); + } + }; + } + + @Rewrite("status:abandoned P=(project:*) S=(sortkey_after:*) L=(limit:*)") + public Predicate r10_byProjectAbandonedPrev( + @Named("P") final ProjectPredicate p, + @Named("S") final SortKeyPredicate.After s, + @Named("L") final IntPredicate l) { + return new PaginatedSource(40000, s.getValue(), l.intValue()) { + @Override + ResultSet scan(ChangeAccess a, String key, int limit) + throws OrmException { + return a.byProjectClosedPrev(Change.Status.ABANDONED.getCode(), // + p.getValueKey(), key, limit); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED + && p.match(cd) // + && s.match(cd); + } + }; + } + + @Rewrite("status:abandoned P=(project:*) S=(sortkey_before:*) L=(limit:*)") + public Predicate r10_byProjectAbandonedNext( + @Named("P") final ProjectPredicate p, + @Named("S") final SortKeyPredicate.Before s, + @Named("L") final IntPredicate l) { + return new PaginatedSource(40000, s.getValue(), l.intValue()) { + @Override + ResultSet scan(ChangeAccess a, String key, int limit) + throws OrmException { + return a.byProjectClosedNext(Change.Status.ABANDONED.getCode(), // + p.getValueKey(), key, limit); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED + && p.match(cd) // + && s.match(cd); + } + }; + } + + @Rewrite("status:open S=(sortkey_after:*) L=(limit:*)") + public Predicate r20_byOpenPrev( + @Named("S") final SortKeyPredicate.After s, + @Named("L") final IntPredicate l) { + return new PaginatedSource(2000, s.getValue(), l.intValue()) { + @Override + ResultSet scan(ChangeAccess a, String key, int limit) + throws OrmException { + return a.allOpenPrev(key, limit); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus().isOpen() && s.match(cd); + } + }; + } + + @Rewrite("status:open S=(sortkey_before:*) L=(limit:*)") + public Predicate r20_byOpenNext( + @Named("S") final SortKeyPredicate.Before s, + @Named("L") final IntPredicate l) { + return new PaginatedSource(2000, s.getValue(), l.intValue()) { + @Override + ResultSet scan(ChangeAccess a, String key, int limit) + throws OrmException { + return a.allOpenNext(key, limit); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus().isOpen() && s.match(cd); + } + }; + } + + @Rewrite("status:merged S=(sortkey_after:*) L=(limit:*)") + public Predicate r20_byMergedPrev( + @Named("S") final SortKeyPredicate.After s, + @Named("L") final IntPredicate l) { + return new PaginatedSource(50000, s.getValue(), l.intValue()) { + @Override + ResultSet scan(ChangeAccess a, String key, int limit) + throws OrmException { + return a.allClosedPrev(Change.Status.MERGED.getCode(), key, limit); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus() == Change.Status.MERGED + && s.match(cd); + } + }; + } + + @Rewrite("status:merged S=(sortkey_before:*) L=(limit:*)") + public Predicate r20_byMergedNext( + @Named("S") final SortKeyPredicate.Before s, + @Named("L") final IntPredicate l) { + return new PaginatedSource(50000, s.getValue(), l.intValue()) { + @Override + ResultSet scan(ChangeAccess a, String key, int limit) + throws OrmException { + return a.allClosedNext(Change.Status.MERGED.getCode(), key, limit); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus() == Change.Status.MERGED + && s.match(cd); + } + }; + } + + @Rewrite("status:abandoned S=(sortkey_after:*) L=(limit:*)") + public Predicate r20_byAbandonedPrev( + @Named("S") final SortKeyPredicate.After s, + @Named("L") final IntPredicate l) { + return new PaginatedSource(50000, s.getValue(), l.intValue()) { + @Override + ResultSet scan(ChangeAccess a, String key, int limit) + throws OrmException { + return a.allClosedPrev(Change.Status.ABANDONED.getCode(), key, limit); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED + && s.match(cd); + } + }; + } + + @Rewrite("status:abandoned S=(sortkey_before:*) L=(limit:*)") + public Predicate r20_byAbandonedNext( + @Named("S") final SortKeyPredicate.Before s, + @Named("L") final IntPredicate l) { + return new PaginatedSource(50000, s.getValue(), l.intValue()) { + @Override + ResultSet scan(ChangeAccess a, String key, int limit) + throws OrmException { + return a.allClosedNext(Change.Status.ABANDONED.getCode(), key, limit); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED + && s.match(cd); + } + }; + } + + @SuppressWarnings("unchecked") + @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)") + public Predicate r20_byClosedPrev( + @Named("S") final SortKeyPredicate.After s, + @Named("L") final IntPredicate l) { + return or(r20_byMergedPrev(s, l), r20_byAbandonedPrev(s, l)); + } + + @SuppressWarnings("unchecked") + @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)") + public Predicate r20_byClosedNext( + @Named("S") final SortKeyPredicate.Before s, + @Named("L") final IntPredicate l) { + return or(r20_byMergedNext(s, l), r20_byAbandonedNext(s, l)); + } + + @Rewrite("status:open O=(owner:*)") + public Predicate r25_byOwnerOpen( + @Named("O") final OwnerPredicate o) { + return new ChangeSource(50) { + @Override + ResultSet scan(ChangeAccess a) throws OrmException { + return a.byOwnerOpen(o.getAccountId()); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus().isOpen() && o.match(cd); + } + }; + } + + @Rewrite("status:closed O=(owner:*)") + public Predicate r25_byOwnerClosed( + @Named("O") final OwnerPredicate o) { + return new ChangeSource(5000) { + @Override + ResultSet scan(ChangeAccess a) throws OrmException { + return a.byOwnerClosedAll(o.getAccountId()); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus().isClosed() && o.match(cd); + } + }; + } + + @SuppressWarnings("unchecked") + @Rewrite("O=(owner:*)") + public Predicate r26_byOwner(@Named("O") OwnerPredicate o) { + return or(r25_byOwnerOpen(o), r25_byOwnerClosed(o)); + } + + @Rewrite("status:open R=(reviewer:*)") + public Predicate r30_byReviewerOpen( + @Named("R") final ReviewerPredicate r) { + return new Source() { + @Override + public ResultSet read() throws OrmException { + return ChangeDataResultSet.patchSetApproval(dbProvider.get() + .patchSetApprovals().openByUser(r.getAccountId())); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + Change change = cd.change(dbProvider); + return change != null && change.getStatus().isOpen() && r.match(cd); + } + + @Override + public int getCardinality() { + return 50; + } + + @Override + public int getCost() { + return ChangeCosts.cost(ChangeCosts.APPROVALS_SCAN, getCardinality()); + } + }; + } + + @Rewrite("status:closed R=(reviewer:*)") + public Predicate r30_byReviewerClosed( + @Named("R") final ReviewerPredicate r) { + return new Source() { + @Override + public ResultSet read() throws OrmException { + return ChangeDataResultSet.patchSetApproval(dbProvider.get() + .patchSetApprovals().closedByUserAll(r.getAccountId())); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + Change change = cd.change(dbProvider); + return change != null && change.getStatus().isClosed() && r.match(cd); + } + + @Override + public int getCardinality() { + return 5000; + } + + @Override + public int getCost() { + return ChangeCosts.cost(ChangeCosts.APPROVALS_SCAN, getCardinality()); + } + }; + } + + @SuppressWarnings("unchecked") + @Rewrite("R=(reviewer:*)") + public Predicate r31_byReviewer( + @Named("R") final ReviewerPredicate r) { + return or(r30_byReviewerOpen(r), r30_byReviewerClosed(r)); + } + + @SuppressWarnings("unchecked") + @Rewrite("status:submitted") + public Predicate r99_allSubmitted() { + return new ChangeSource(50) { + @Override + ResultSet scan(ChangeAccess a) throws OrmException { + return a.allSubmitted(); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.change(dbProvider).getStatus() == Change.Status.SUBMITTED; + } + }; + } + + @SuppressWarnings("unchecked") + @Rewrite("P=(project:*)") + public Predicate r99_byProject( + @Named("P") final ProjectPredicate p) { + return new ChangeSource(1000000) { + @Override + ResultSet scan(ChangeAccess a) throws OrmException { + return a.byProject(p.getValueKey()); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return p.match(cd); + } + }; + } + + private static boolean hasSource(Collection> l) { + for (Predicate p : l) { + if (p instanceof ChangeDataSource) { + return true; + } + } + return false; + } + + private abstract static class Source extends RewritePredicate + implements ChangeDataSource { + @Override + public boolean hasChange() { + return false; + } + } + + private abstract class ChangeSource extends Source { + private final int cardinality; + + ChangeSource(int card) { + this.cardinality = card; + } + + abstract ResultSet scan(ChangeAccess a) throws OrmException; + + @Override + public ResultSet read() throws OrmException { + return ChangeDataResultSet.change(scan(dbProvider.get().changes())); + } + + @Override + public boolean hasChange() { + return true; + } + + @Override + public int getCardinality() { + return cardinality; + } + + @Override + public int getCost() { + return ChangeCosts.cost(ChangeCosts.CHANGES_SCAN, getCardinality()); + } + } + + private abstract class PaginatedSource extends ChangeSource implements + Paginated { + private final String startKey; + private final int limit; + + PaginatedSource(int card, String start, int lim) { + super(card); + this.startKey = start; + this.limit = lim; + } + + @Override + public int limit() { + return limit; + } + + @Override + ResultSet scan(ChangeAccess a) throws OrmException { + return scan(a, startKey, limit); + } + + @Override + public ResultSet restart(ChangeData last) throws OrmException { + return ChangeDataResultSet.change(scan(dbProvider.get().changes(), // + last.change(dbProvider).getSortKey(), // + limit)); + } + + abstract ResultSet scan(ChangeAccess a, String key, int limit) + throws OrmException; + } + + private static final class InvalidProvider implements Provider { + @Override + public T get() { + throw new OutOfScopeException("Not available at init"); + } + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java new file mode 100644 index 0000000000..4ae2278771 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java @@ -0,0 +1,126 @@ +// Copyright (C) 2009 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.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gerrit.server.query.Predicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +/** + * Predicate for a {@link Change.Status}. + *

+ * The actual name of this operator can differ, it usually comes as {@code + * status:} but may also be {@code is:} to help do-what-i-meanery for end-users + * searching for changes. Either operator name has the same meaning. + */ +final class ChangeStatusPredicate extends OperatorPredicate { + private static final Map byName; + private static final EnumMap byEnum; + + static { + byName = new HashMap(); + byEnum = new EnumMap(Change.Status.class); + for (final Change.Status s : Change.Status.values()) { + final String name = s.name().toLowerCase(); + byName.put(name, s); + byEnum.put(s, name); + } + } + + static Predicate open(Provider dbProvider) { + List> r = new ArrayList>(4); + for (final Change.Status e : Change.Status.values()) { + if (e.isOpen()) { + r.add(new ChangeStatusPredicate(dbProvider, e)); + } + } + return r.size() == 1 ? r.get(0) : or(r); + } + + static Predicate closed(Provider dbProvider) { + List> r = new ArrayList>(4); + for (final Change.Status e : Change.Status.values()) { + if (e.isClosed()) { + r.add(new ChangeStatusPredicate(dbProvider, e)); + } + } + return r.size() == 1 ? r.get(0) : or(r); + } + + private static Change.Status parse(final String value) { + final Change.Status s = byName.get(value); + if (s == null) { + throw new IllegalArgumentException(); + } + return s; + } + + private final Provider dbProvider; + private final Change.Status status; + + ChangeStatusPredicate(Provider dbProvider, String value) { + this(dbProvider, parse(value)); + } + + ChangeStatusPredicate(Provider dbProvider, Change.Status status) { + super(ChangeQueryBuilder.FIELD_STATUS, byEnum.get(status)); + this.dbProvider = dbProvider; + this.status = status; + } + + Change.Status getStatus() { + return status; + } + + @Override + public boolean match(final ChangeData object) throws OrmException { + Change change = object.change(dbProvider); + return change != null && status.equals(change.getStatus()); + } + + @Override + public int getCost() { + return 0; + } + + @Override + public int hashCode() { + return status.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other instanceof ChangeStatusPredicate) { + final ChangeStatusPredicate p = (ChangeStatusPredicate) other; + return status.equals(p.status); + } + return false; + } + + @Override + public String toString() { + return getOperator() + ":" + getValue(); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java new file mode 100644 index 0000000000..c03cddc85b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java @@ -0,0 +1,77 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.PatchSet; +import com.google.gerrit.reviewdb.RevId; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.ObjectIdPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.gwtorm.client.ResultSet; +import com.google.inject.Provider; + +import org.eclipse.jgit.lib.AbbreviatedObjectId; +import org.eclipse.jgit.lib.ObjectId; + +class CommitPredicate extends ObjectIdPredicate implements + ChangeDataSource { + private final Provider dbProvider; + + CommitPredicate(Provider dbProvider, AbbreviatedObjectId id) { + super(ChangeQueryBuilder.FIELD_COMMIT, id); + this.dbProvider = dbProvider; + } + + @Override + public boolean match(final ChangeData object) throws OrmException { + for (PatchSet p : object.patches(dbProvider)) { + if (p.getRevision() != null && p.getRevision().get() != null) { + final ObjectId id = ObjectId.fromString(p.getRevision().get()); + if (abbreviated().prefixCompare(id) == 0) { + return true; + } + } + } + return false; + } + + @Override + public ResultSet read() throws OrmException { + final RevId id = new RevId(abbreviated().name()); + if (id.isComplete()) { + return ChangeDataResultSet.patchSet(// + dbProvider.get().patchSets().byRevision(id)); + + } else { + return ChangeDataResultSet.patchSet(// + dbProvider.get().patchSets().byRevisionRange(id, id.max())); + } + } + + @Override + public boolean hasChange() { + return false; + } + + @Override + public int getCardinality() { + return ChangeCosts.CARD_COMMIT; + } + + @Override + public int getCost() { + return ChangeCosts.cost(ChangeCosts.PATCH_SETS_SCAN, getCardinality()); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java new file mode 100644 index 0000000000..07d4dd245e --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java @@ -0,0 +1,81 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.Account; +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.PatchLineComment; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.gwtorm.client.ResultSet; +import com.google.gwtorm.client.impl.ListResultSet; +import com.google.inject.Provider; + +import java.util.ArrayList; +import java.util.HashSet; + +class HasDraftByPredicate extends OperatorPredicate implements + ChangeDataSource { + private final Provider db; + private final Account.Id accountId; + + HasDraftByPredicate(Provider db, Account.Id accountId) { + super(ChangeQueryBuilder.FIELD_DRAFTBY, accountId.toString()); + this.db = db; + this.accountId = accountId; + } + + @Override + public boolean match(final ChangeData object) throws OrmException { + for (PatchLineComment c : object.comments(db)) { + if (c.getStatus() == PatchLineComment.Status.DRAFT + && c.getAuthor().equals(accountId)) { + return true; + } + } + return false; + } + + @Override + public ResultSet read() throws OrmException { + HashSet ids = new HashSet(); + for (PatchLineComment sc : db.get().patchComments() + .draftByAuthor(accountId)) { + ids.add(sc.getKey().getParentKey().getParentKey().getParentKey()); + } + + ArrayList r = new ArrayList(ids.size()); + for (Change.Id id : ids) { + r.add(new ChangeData(id)); + } + return new ListResultSet(r); + } + + @Override + public boolean hasChange() { + return false; + } + + @Override + public int getCardinality() { + return 20; + } + + @Override + public int getCost() { + return ChangeCosts.cost(ChangeCosts.PATCH_SETS_SCAN, getCardinality()); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java new file mode 100644 index 0000000000..46c774106d --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java @@ -0,0 +1,54 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.PatchSet; +import com.google.gerrit.reviewdb.PatchSetApproval; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +class IsReviewedPredicate extends OperatorPredicate { + private final Provider dbProvider; + + IsReviewedPredicate(Provider dbProvider) { + super(ChangeQueryBuilder.FIELD_IS, "reviewed"); + this.dbProvider = dbProvider; + } + + @Override + public boolean match(final ChangeData object) throws OrmException { + Change c = object.change(dbProvider); + if (c == null) { + return false; + } + + PatchSet.Id current = c.currentPatchSetId(); + for (PatchSetApproval p : object.approvals(dbProvider)) { + if (p.getPatchSetId().equals(current) && p.getValue() != 0) { + return true; + } + } + + return false; + } + + @Override + public int getCost() { + return 2; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java new file mode 100644 index 0000000000..aaf8478586 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java @@ -0,0 +1,68 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.gwtorm.client.ResultSet; +import com.google.inject.Provider; + +class IsStarredByPredicate extends OperatorPredicate implements + ChangeDataSource { + private static String describe(CurrentUser user) { + if (user instanceof IdentifiedUser) { + return ((IdentifiedUser) user).getAccountId().toString(); + } + return user.toString(); + } + + private final Provider db; + private final CurrentUser user; + + IsStarredByPredicate(Provider db, CurrentUser user) { + super(ChangeQueryBuilder.FIELD_STARREDBY, describe(user)); + this.db = db; + this.user = user; + } + + @Override + public boolean match(final ChangeData object) { + return user.getStarredChanges().contains(object.getId()); + } + + @Override + public ResultSet read() throws OrmException { + return ChangeDataResultSet.change( // + db.get().changes().get(user.getStarredChanges())); + } + + @Override + public boolean hasChange() { + return true; + } + + @Override + public int getCardinality() { + return 10; + } + + @Override + public int getCost() { + return ChangeCosts.cost(ChangeCosts.IDS_MEMORY, getCardinality()); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java new file mode 100644 index 0000000000..020e709514 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java @@ -0,0 +1,73 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.project.NoSuchChangeException; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +class IsVisibleToPredicate extends OperatorPredicate { + private static String describe(CurrentUser user) { + if (user instanceof IdentifiedUser) { + return ((IdentifiedUser) user).getAccountId().toString(); + } + if (user instanceof SingleGroupUser) { + return "group:" + ((SingleGroupUser) user).getEffectiveGroups() // + .iterator().next().toString(); + } + return user.toString(); + } + + private final Provider db; + private final ChangeControl.Factory changeControl; + private final CurrentUser user; + + IsVisibleToPredicate(Provider db, + ChangeControl.Factory changeControlFactory, CurrentUser user) { + super(ChangeQueryBuilder.FIELD_VISIBLETO, describe(user)); + this.db = db; + this.changeControl = changeControlFactory; + this.user = user; + } + + @Override + public boolean match(final ChangeData cd) throws OrmException { + if (cd.fastIsVisibleTo(user)) { + return true; + } + try { + Change c = cd.change(db); + if (c != null && changeControl.controlFor(c).forUser(user).isVisible()) { + cd.cacheVisibleTo(user); + return true; + } else { + return false; + } + } catch (NoSuchChangeException e) { + return false; + } + } + + @Override + public int getCost() { + return 1; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java new file mode 100644 index 0000000000..429b07b21d --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java @@ -0,0 +1,70 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.Project; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.reviewdb.Project.NameKey; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.config.WildProjectName; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +import java.util.Set; + +class IsWatchedByPredicate extends OperatorPredicate { + private static String describe(CurrentUser user) { + if (user instanceof IdentifiedUser) { + return ((IdentifiedUser) user).getAccountId().toString(); + } + return user.toString(); + } + + private final Provider db; + private final Project.NameKey wildProject; + private final CurrentUser user; + + IsWatchedByPredicate(Provider db, + @WildProjectName Project.NameKey wildProject, CurrentUser user) { + super(ChangeQueryBuilder.FIELD_WATCHEDBY, describe(user)); + this.db = db; + this.wildProject = wildProject; + this.user = user; + } + + @Override + public boolean match(final ChangeData cd) throws OrmException { + Set watched = user.getWatchedProjects(); + if (watched.contains(wildProject)) { + return true; + } + + Change change = cd.change(db); + if (change == null) { + return false; + } + + Project.NameKey project = change.getDest().getParentKey(); + return watched.contains(project); + } + + @Override + public int getCost() { + return 1; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java new file mode 100644 index 0000000000..0d9e055fa8 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java @@ -0,0 +1,153 @@ +// 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.query.change; + +import com.google.gerrit.common.data.ApprovalType; +import com.google.gerrit.common.data.ApprovalTypes; +import com.google.gerrit.reviewdb.ApprovalCategory; +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.PatchSet; +import com.google.gerrit.reviewdb.PatchSetApproval; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class LabelPredicate extends OperatorPredicate { + private static enum Test { + EQ { + @Override + public boolean match(PatchSetApproval p, short value) { + return p.getValue() == value; + } + }, + GT_EQ { + @Override + public boolean match(PatchSetApproval p, short value) { + return p.getValue() >= value; + } + }, + LT_EQ { + @Override + public boolean match(PatchSetApproval p, short value) { + return p.getValue() <= value; + } + }; + + abstract boolean match(PatchSetApproval p, short value); + } + + private static ApprovalCategory.Id category(ApprovalTypes types, String toFind) { + if (types.getApprovalType(new ApprovalCategory.Id(toFind)) != null) { + return new ApprovalCategory.Id(toFind); + } + + for (ApprovalType at : types.getApprovalTypes()) { + String name = at.getCategory().getName(); + if (toFind.equalsIgnoreCase(name)) { + return at.getCategory().getId(); + + } else if (toFind.equalsIgnoreCase(name.replace(" ", ""))) { + return at.getCategory().getId(); + } + } + + for (ApprovalType at : types.getApprovalTypes()) { + if (toFind.equalsIgnoreCase(at.getCategory().getAbbreviatedName())) { + return at.getCategory().getId(); + } + } + + return new ApprovalCategory.Id(toFind); + } + + private static Test op(String op) { + if ("=".equals(op)) { + return Test.EQ; + + } else if (">=".equals(op)) { + return Test.GT_EQ; + + } else if ("<=".equals(op)) { + return Test.LT_EQ; + + } else { + throw new IllegalArgumentException("Unsupported operation " + op); + } + } + + private static short value(String value) { + if (value.startsWith("+")) { + value = value.substring(1); + } + return Short.parseShort(value); + } + + private final Provider dbProvider; + private final Test test; + private final ApprovalCategory.Id category; + private final short val; + + LabelPredicate(Provider dbProvider, ApprovalTypes types, + String value) { + super(ChangeQueryBuilder.FIELD_LABEL, value); + this.dbProvider = dbProvider; + + Matcher m1 = Pattern.compile("(=|>=|<=)([+-]?\\d+)$").matcher(value); + Matcher m2 = Pattern.compile("([+-]\\d+)$").matcher(value); + if (m1.find()) { + category = category(types, value.substring(0, m1.start())); + test = op(m1.group(1)); + val = value(m1.group(2)); + + } else if (m2.find()) { + category = category(types, value.substring(0, m2.start())); + test = Test.EQ; + val = value(m2.group(1)); + + } else { + category = category(types, value); + test = Test.EQ; + val = 1; + } + } + + @Override + public boolean match(final ChangeData object) throws OrmException { + Change c = object.change(dbProvider); + if (c == null) { + return false; + } + + PatchSet.Id current = c.currentPatchSetId(); + for (PatchSetApproval p : object.approvals(dbProvider)) { + if (p.getPatchSetId().equals(current) + && p.getCategoryId().equals(category) // + && test.match(p, val)) { + return true; + } + } + + return false; + } + + @Override + public int getCost() { + return 2; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java new file mode 100644 index 0000000000..ac544a3a49 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java @@ -0,0 +1,68 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.gwtorm.client.ResultSet; +import com.google.gwtorm.client.impl.ListResultSet; +import com.google.inject.Provider; + +import java.util.Collections; + +class LegacyChangeIdPredicate extends OperatorPredicate implements + ChangeDataSource { + private final Provider db; + private final Change.Id id; + + LegacyChangeIdPredicate(Provider db, Change.Id id) { + super(ChangeQueryBuilder.FIELD_CHANGE, id.toString()); + this.db = db; + this.id = id; + } + + @Override + public boolean match(final ChangeData object) { + return id.equals(object.getId()); + } + + @Override + public ResultSet read() throws OrmException { + Change c = db.get().changes().get(id); + if (c != null) { + return new ListResultSet( // + Collections.singletonList(new ChangeData(c))); + } else { + return new ListResultSet(Collections. emptyList()); + } + } + + @Override + public boolean hasChange() { + return true; + } + + @Override + public int getCardinality() { + return 1; + } + + @Override + public int getCost() { + return ChangeCosts.IDS_MEMORY; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java new file mode 100644 index 0000000000..617a14ad6a --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java @@ -0,0 +1,78 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.server.query.OrPredicate; +import com.google.gerrit.server.query.Predicate; +import com.google.gwtorm.client.OrmException; +import com.google.gwtorm.client.ResultSet; +import com.google.gwtorm.client.impl.ListResultSet; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; + +class OrSource extends OrPredicate implements ChangeDataSource { + private int cardinality = -1; + + OrSource(final Collection> that) { + super(that); + } + + @Override + public ResultSet read() throws OrmException { + // TODO(spearce) This probably should be more lazy. + // + ArrayList r = new ArrayList(); + HashSet have = new HashSet(); + for (Predicate p : getChildren()) { + if (p instanceof ChangeDataSource) { + for (ChangeData cd : ((ChangeDataSource) p).read()) { + if (have.add(cd.getId())) { + r.add(cd); + } + } + } else { + throw new OrmException("No ChangeDataSource: " + p); + } + } + return new ListResultSet(r); + } + + @Override + public boolean hasChange() { + for (Predicate p : getChildren()) { + if (!(p instanceof ChangeDataSource) + || !((ChangeDataSource) p).hasChange()) { + return false; + } + } + return true; + } + + @Override + public int getCardinality() { + if (cardinality < 0) { + cardinality = 0; + for (Predicate p : getChildren()) { + if (p instanceof ChangeDataSource) { + cardinality += ((ChangeDataSource) p).getCardinality(); + } + } + } + return cardinality; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java new file mode 100644 index 0000000000..224dce98e2 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java @@ -0,0 +1,48 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.Account; +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +class OwnerPredicate extends OperatorPredicate { + private final Provider dbProvider; + private final Account.Id id; + + OwnerPredicate(Provider dbProvider, Account.Id id) { + super(ChangeQueryBuilder.FIELD_OWNER, id.toString()); + this.dbProvider = dbProvider; + this.id = id; + } + + Account.Id getAccountId() { + return id; + } + + @Override + public boolean match(final ChangeData object) throws OrmException { + Change change = object.change(dbProvider); + return change != null && id.equals(change.getOwner()); + } + + @Override + public int getCost() { + return 1; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java new file mode 100644 index 0000000000..b046db620e --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java @@ -0,0 +1,24 @@ +// 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.query.change; + +import com.google.gwtorm.client.OrmException; +import com.google.gwtorm.client.ResultSet; + +interface Paginated { + int limit(); + + ResultSet restart(ChangeData last) throws OrmException; +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java new file mode 100644 index 0000000000..91203d6b15 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java @@ -0,0 +1,51 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.Project; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +class ProjectPredicate extends OperatorPredicate { + private final Provider dbProvider; + + ProjectPredicate(Provider dbProvider, String id) { + super(ChangeQueryBuilder.FIELD_PROJECT, id); + this.dbProvider = dbProvider; + } + + Project.NameKey getValueKey() { + return new Project.NameKey(getValue()); + } + + @Override + public boolean match(final ChangeData object) throws OrmException { + Change change = object.change(dbProvider); + if (change == null) { + return false; + } + + Project.NameKey p = change.getDest().getParentKey(); + return p.equals(getValueKey()); + } + + @Override + public int getCost() { + return 1; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java new file mode 100644 index 0000000000..f5f83e21bd --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java @@ -0,0 +1,44 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +class RefPredicate extends OperatorPredicate { + private final Provider dbProvider; + + RefPredicate(Provider dbProvider, String ref) { + super(ChangeQueryBuilder.FIELD_REF, ref); + this.dbProvider = dbProvider; + } + + @Override + public boolean match(final ChangeData object) throws OrmException { + Change change = object.change(dbProvider); + if (change == null) { + return false; + } + return getValue().equals(change.getDest().get()); + } + + @Override + public int getCost() { + return 1; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java new file mode 100644 index 0000000000..bcece94119 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java @@ -0,0 +1,52 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.Account; +import com.google.gerrit.reviewdb.PatchSetApproval; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +class ReviewerPredicate extends OperatorPredicate { + private final Provider dbProvider; + private final Account.Id id; + + ReviewerPredicate(Provider dbProvider, Account.Id id) { + super(ChangeQueryBuilder.FIELD_REVIEWER, id.toString()); + this.dbProvider = dbProvider; + this.id = id; + } + + Account.Id getAccountId() { + return id; + } + + @Override + public boolean match(final ChangeData object) throws OrmException { + for (PatchSetApproval p : object.approvals(dbProvider)) { + if (id.equals(p.getAccountId())) { + return true; + } + } + return false; + } + + @Override + public int getCost() { + return 2; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java new file mode 100644 index 0000000000..36e04d97a6 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java @@ -0,0 +1,53 @@ +// Copyright (C) 2009 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.query.change; + +import com.google.gerrit.reviewdb.AccountGroup; +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.Project; +import com.google.gerrit.server.AccessPath; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.config.AuthConfig; + +import java.util.Collections; +import java.util.Set; + +final class SingleGroupUser extends CurrentUser { + private final Set groups; + + SingleGroupUser(AuthConfig authConfig, AccountGroup.Id groupId) { + this(authConfig, Collections.singleton(groupId)); + } + + SingleGroupUser(AuthConfig authConfig, Set groups) { + super(AccessPath.UNKNOWN, authConfig); + this.groups = groups; + } + + @Override + public Set getEffectiveGroups() { + return groups; + } + + @Override + public Set getStarredChanges() { + return Collections.emptySet(); + } + + @Override + public Set getWatchedProjects() { + return Collections.emptySet(); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java new file mode 100644 index 0000000000..3ecc5969d8 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java @@ -0,0 +1,47 @@ +// Copyright 2010 Google Inc. All Rights Reserved. + +package com.google.gerrit.server.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +abstract class SortKeyPredicate extends OperatorPredicate { + protected final Provider dbProvider; + + SortKeyPredicate(Provider dbProvider, String name, String value) { + super(name, value); + this.dbProvider = dbProvider; + } + + @Override + public int getCost() { + return 1; + } + + static class Before extends SortKeyPredicate { + Before(Provider dbProvider, String value) { + super(dbProvider, "sortkey_before", value); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + Change change = cd.change(dbProvider); + return change != null && change.getSortKey().compareTo(getValue()) < 0; + } + } + + static class After extends SortKeyPredicate { + After(Provider dbProvider, String value) { + super(dbProvider, "sortkey_after", value); + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + Change change = cd.change(dbProvider); + return change != null && change.getSortKey().compareTo(getValue()) > 0; + } + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java new file mode 100644 index 0000000000..7bc972d7c3 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java @@ -0,0 +1,44 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +class TopicPredicate extends OperatorPredicate { + private final Provider dbProvider; + + TopicPredicate(Provider dbProvider, String topic) { + super(ChangeQueryBuilder.FIELD_TOPIC, topic); + this.dbProvider = dbProvider; + } + + @Override + public boolean match(final ChangeData object) throws OrmException { + Change change = object.change(dbProvider); + if (change == null) { + return false; + } + return getValue().equals(change.getTopic()); + } + + @Override + public int getCost() { + return 1; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java new file mode 100644 index 0000000000..eef568d874 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java @@ -0,0 +1,77 @@ +// 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.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.reviewdb.TrackingId; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.gwtorm.client.ResultSet; +import com.google.gwtorm.client.impl.ListResultSet; +import com.google.inject.Provider; + +import java.util.ArrayList; +import java.util.HashSet; + +class TrackingIdPredicate extends OperatorPredicate implements + ChangeDataSource { + private final Provider db; + + TrackingIdPredicate(Provider db, String trackingId) { + super(ChangeQueryBuilder.FIELD_TR, trackingId); + this.db = db; + } + + @Override + public boolean match(final ChangeData object) throws OrmException { + for (TrackingId c : object.trackingIds(db)) { + if (getValue().equals(c.getTrackingId())) { + return true; + } + } + return false; + } + + @Override + public ResultSet read() throws OrmException { + HashSet ids = new HashSet(); + for (TrackingId sc : db.get().trackingIds() // + .byTrackingId(new TrackingId.Id(getValue()))) { + ids.add(sc.getChangeId()); + } + + ArrayList r = new ArrayList(ids.size()); + for (Change.Id id : ids) { + r.add(new ChangeData(id)); + } + return new ListResultSet(r); + } + + @Override + public boolean hasChange() { + return false; + } + + @Override + public int getCardinality() { + return ChangeCosts.CARD_TRACKING_IDS; + } + + @Override + public int getCost() { + return ChangeCosts.cost(ChangeCosts.TR_SCAN, getCardinality()); + } +} diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/AndPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/AndPredicateTest.java new file mode 100644 index 0000000000..589440dcea --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/AndPredicateTest.java @@ -0,0 +1,138 @@ +// Copyright (C) 2009 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.query; + +import static com.google.gerrit.server.query.Predicate.and; + +import junit.framework.TestCase; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class AndPredicateTest extends TestCase { + private static final class TestPredicate extends OperatorPredicate { + private TestPredicate(String name, String value) { + super(name, value); + } + + @Override + public boolean match(String object) { + return false; + } + + @Override + public int getCost() { + return 0; + } + } + + private static TestPredicate f(final String name, final String value) { + return new TestPredicate(name, value); + } + + public void testChildren() { + final TestPredicate a = f("author", "alice"); + final TestPredicate b = f("author", "bob"); + final Predicate n = and(a, b); + assertEquals(2, n.getChildCount()); + assertSame(a, n.getChild(0)); + assertSame(b, n.getChild(1)); + } + + public void testChildrenUnmodifiable() { + final TestPredicate a = f("author", "alice"); + final TestPredicate b = f("author", "bob"); + final Predicate n = and(a, b); + + try { + n.getChildren().clear(); + } catch (RuntimeException e) { + } + assertChildren("clear", n, list(a, b)); + + try { + n.getChildren().remove(0); + } catch (RuntimeException e) { + } + assertChildren("remove(0)", n, list(a, b)); + + try { + n.getChildren().iterator().remove(); + } catch (RuntimeException e) { + } + assertChildren("remove(0)", n, list(a, b)); + } + + private static void assertChildren(String o, Predicate p, + final List> l) { + assertEquals(o + " did not affect child", l, p.getChildren()); + } + + public void testToString() { + final TestPredicate a = f("q", "alice"); + final TestPredicate b = f("q", "bob"); + final TestPredicate c = f("q", "charlie"); + assertEquals("(q:alice q:bob)", and(a, b).toString()); + assertEquals("(q:alice q:bob q:charlie)", and(a, b, c).toString()); + } + + public void testEquals() { + final TestPredicate a = f("author", "alice"); + final TestPredicate b = f("author", "bob"); + final TestPredicate c = f("author", "charlie"); + + assertTrue(and(a, b).equals(and(a, b))); + assertTrue(and(a, b, c).equals(and(a, b, c))); + + assertFalse(and(a, b).equals(and(b, a))); + assertFalse(and(a, c).equals(and(a, b))); + + assertFalse(and(a, c).equals(a)); + } + + public void testHashCode() { + final TestPredicate a = f("author", "alice"); + final TestPredicate b = f("author", "bob"); + final TestPredicate c = f("author", "charlie"); + + assertTrue(and(a, b).hashCode() == and(a, b).hashCode()); + assertTrue(and(a, b, c).hashCode() == and(a, b, c).hashCode()); + assertFalse(and(a, c).hashCode() == and(a, b).hashCode()); + } + + public void testCopy() { + final TestPredicate a = f("author", "alice"); + final TestPredicate b = f("author", "bob"); + final TestPredicate c = f("author", "charlie"); + final List> s2 = list(a, b); + final List> s3 = list(a, b, c); + final Predicate n2 = and(a, b); + + assertNotSame(n2, n2.copy(s2)); + assertEquals(s2, n2.copy(s2).getChildren()); + assertEquals(s3, n2.copy(s3).getChildren()); + + try { + n2.copy(Collections.> emptyList()); + } catch (IllegalArgumentException e) { + assertEquals("Need at least two predicates", e.getMessage()); + } + } + + private static List> list(final Predicate... predicates) { + return Arrays.asList(predicates); + } +} diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/ChangeQueryBuilderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/ChangeQueryBuilderTest.java deleted file mode 100644 index 4dc8ba5ad8..0000000000 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/ChangeQueryBuilderTest.java +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (C) 2009 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.query; - -import static com.google.gerrit.server.query.ChangeQueryBuilder.FIELD_CHANGE; -import static com.google.gerrit.server.query.ChangeQueryBuilder.FIELD_COMMIT; -import static com.google.gerrit.server.query.ChangeQueryBuilder.FIELD_OWNER; -import static com.google.gerrit.server.query.ChangeQueryBuilder.FIELD_REVIEWER; -import static com.google.gerrit.server.query.Predicate.and; -import static com.google.gerrit.server.query.Predicate.not; -import static com.google.gerrit.server.query.Predicate.or; - -import junit.framework.TestCase; - -import org.eclipse.jgit.lib.AbbreviatedObjectId; - -public class ChangeQueryBuilderTest extends TestCase { - private static OperatorPredicate f(final String name, final String value) { - return new OperatorPredicate(name, value); - } - - private static Predicate owner(final String who) { - return f(FIELD_OWNER, who); - } - - private static Predicate reviewer(final String who) { - return f(FIELD_REVIEWER, who); - } - - private static Predicate commit(final String idstr) { - final AbbreviatedObjectId id = AbbreviatedObjectId.fromString(idstr); - return new ObjectIdPredicate(FIELD_COMMIT, id); - } - - private static Predicate p(final String str) throws QueryParseException { - return new ChangeQueryBuilder().parse(str); - } - - public void testEmptyQuery() { - try { - p(""); - fail("expected exception"); - } catch (QueryParseException e) { - assertEquals("line 0:-1 no viable alternative at input ''", e - .getMessage()); - } - } - - public void testFailInvalidOperator() { - final String op = "thiswillneverbeaqueryoperatoritistoolongtotype"; - final String val = "true"; - try { - p(op + ":" + val); - fail("expected exception"); - } catch (QueryParseException e) { - assertEquals("Unsupported operator " + op + ":" + val, e.getMessage()); - } - } - - public void testFailNestedOperator() { - try { - p("commit:(foo:bar whiz:bang)"); - fail("expected exception"); - } catch (QueryParseException e) { - assertEquals("Nested operator not expected: foo", e.getMessage()); - } - } - - // commit: - - public void testDefaultSHA1() throws QueryParseException { - assertEquals(commit("6ea15"), p("6ea15")); - assertEquals(commit("6ea15"), p("6EA15")); - assertEquals(commit("6ea15b73668073fd9f70b2635efcb8cf8aabda22"), - p("6ea15b73668073fd9f70b2635efcb8cf8aabda22")); - } - - public void testCommitSHA1() throws QueryParseException { - assertEquals(commit("6ea15"), p("commit:6ea15")); - assertEquals(commit("6ea15"), p("commit:6EA15")); // note: forces lowercase - assertEquals(commit("6ea15b73668073fd9f70b2635efcb8cf8aabda22"), - p("commit:6ea15b73668073fd9f70b2635efcb8cf8aabda22")); - - try { - p("commit:yonothash"); - } catch (QueryParseException e) { - assertEquals("Error in operator commit:yonothash", e.getMessage()); - } - } - - // change: - - public void testDefaultChangeID() throws QueryParseException { - assertEquals(f(FIELD_CHANGE, "1234"), p("1234")); - } - - public void testChangeID() throws QueryParseException { - assertEquals(f(FIELD_CHANGE, "1234"), p("change:1234")); - } - - // owner: - - public void testOwnerBare() throws QueryParseException { - assertEquals(owner("bob"), p("owner:bob")); - assertEquals(owner("Bob"), p("owner:Bob")); - assertEquals(owner("bob@example.com"), p("owner:bob@example.com")); - - assertEquals(owner("bob"), p("owner: bob")); - assertEquals(owner("Bob"), p("owner: Bob")); - assertEquals(owner("bob@example.com"), p("owner: bob@example.com")); - - assertEquals(owner("bob"), p("owner:\tbob")); - assertEquals(owner("Bob"), p("owner:\tBob")); - assertEquals(owner("bob@example.com"), p("owner:\tbob@example.com")); - } - - public void testOwnerQuoted() throws QueryParseException { - assertEquals(owner("bob"), p("owner:\"bob\"")); - assertEquals(owner("bob@example.com"), p("owner:\"bob@example.com\"")); - assertEquals(owner(""), p("owner:\"\"")); - assertEquals(owner("A U Thor"), p("owner:\"A U Thor\"")); - - assertEquals(owner("bob"), p("owner: \"bob\"")); - assertEquals(owner("bob@example.com"), p("owner: \"bob@example.com\"")); - assertEquals(owner(""), p("owner: \"\"")); - assertEquals(owner("A U Thor"), p("owner: \"A U Thor\"")); - - assertEquals(owner("bob"), p("owner:\t\"bob\"")); - assertEquals(owner("bob@example.com"), p("owner:\t\"bob@example.com\"")); - assertEquals(owner(""), p("owner:\t\"\"")); - assertEquals(owner("A U Thor"), p("owner:\t\"A U Thor\"")); - } - - public void testOwner_NOT() throws QueryParseException { - assertEquals(not(owner("bob")), p("-owner:bob")); - assertEquals(not(owner("Bob")), p("-owner:Bob")); - assertEquals(not(owner("bob@example.com")), p("-owner:bob@example.com")); - - assertEquals(not(owner("bob")), p("NOT owner:bob")); - assertEquals(not(owner("Bob")), p("NOT owner:Bob")); - assertEquals(not(owner("bob@example.com")), p("NOT owner:bob@example.com")); - } - - // AND - - public void testAND_Styles2() throws QueryParseException { - final Predicate exp = and(commit("6ea15"), owner("bob")); - assertEquals(exp, p("6ea15 owner:bob")); - assertEquals(exp, p("6ea15 AND owner:bob")); - } - - public void testAND_Styles3() throws QueryParseException { - final Predicate exp = and(commit("6ea15"), owner("bob"), reviewer("alice")); - assertEquals(exp, p("6ea15 owner:bob reviewer:alice")); - assertEquals(exp, p("6ea15 AND owner:bob reviewer:alice")); - assertEquals(exp, p("6ea15 owner:bob AND reviewer:alice")); - assertEquals(exp, p("6ea15 AND owner:bob AND reviewer:alice")); - } - - public void testAND_ManyValuesOneOperator() throws QueryParseException { - final Predicate exp = - and(reviewer("alice"), reviewer("bob"), reviewer("charlie")); - assertEquals(exp, p("reviewer:(alice bob charlie)")); - assertEquals(exp, p("reviewer:(alice AND bob charlie)")); - assertEquals(exp, p("reviewer:(alice bob AND charlie)")); - assertEquals(exp, p("reviewer:(alice AND bob AND charlie)")); - } - - public void testAND_FlattensOperators() throws QueryParseException { - final Predicate exp = - and(reviewer("alice"), reviewer("bob"), reviewer("charlie")); - assertEquals(exp, p("reviewer:alice reviewer:(bob charlie)")); - } - - // OR - - public void testOR_2() throws QueryParseException { - final Predicate exp = or(commit("6ea15"), owner("bob")); - assertEquals(exp, p("6ea15 OR owner:bob")); - } - - public void testOR_3() throws QueryParseException { - final Predicate exp = or(commit("6ea15"), owner("bob"), reviewer("alice")); - assertEquals(exp, p("6ea15 OR owner:bob OR reviewer:alice")); - } - - public void testOR_ManyValuesOneOperator() throws QueryParseException { - final Predicate exp = - or(reviewer("alice"), reviewer("bob"), reviewer("charlie")); - assertEquals(exp, p("reviewer:(alice OR bob OR charlie)")); - } - - public void testOR_FlattensOperators() throws QueryParseException { - final Predicate exp = - or(reviewer("alice"), reviewer("bob"), reviewer("charlie")); - assertEquals(exp, p("reviewer:alice OR reviewer:(bob OR charlie)")); - } -} diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java index 63cf166951..a37a336828 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java @@ -16,9 +16,27 @@ package com.google.gerrit.server.query; import junit.framework.TestCase; +import java.util.Collections; + public class FieldPredicateTest extends TestCase { - private static OperatorPredicate f(final String name, final String value) { - return new OperatorPredicate(name, value); + private static final class TestPredicate extends OperatorPredicate { + private TestPredicate(String name, String value) { + super(name, value); + } + + @Override + public boolean match(String object) { + return false; + } + + @Override + public int getCost() { + return 0; + } + } + + private static TestPredicate f(final String name, final String value) { + return new TestPredicate(name, value); } public void testToString() { @@ -42,9 +60,21 @@ public class FieldPredicateTest extends TestCase { public void testNameValue() { final String name = "author"; final String value = "alice"; - final OperatorPredicate f = f(name, value); + final OperatorPredicate f = f(name, value); assertSame(name, f.getOperator()); assertSame(value, f.getValue()); assertEquals(0, f.getChildren().size()); } + + public void testCopy() { + final OperatorPredicate f = f("author", "alice"); + assertSame(f, f.copy(Collections.> emptyList())); + assertSame(f, f.copy(f.getChildren())); + + try { + f.copy(Collections.singleton(f("owner", "bob"))); + } catch (IllegalArgumentException e) { + assertEquals("Expected 0 children", e.getMessage()); + } + } } diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java index 23ba24ed81..8e86e95231 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java @@ -14,33 +14,53 @@ package com.google.gerrit.server.query; +import static com.google.gerrit.server.query.Predicate.and; import static com.google.gerrit.server.query.Predicate.not; import junit.framework.TestCase; +import java.util.Collections; +import java.util.List; + public class NotPredicateTest extends TestCase { - private static OperatorPredicate f(final String name, final String value) { - return new OperatorPredicate(name, value); + private static final class TestPredicate extends OperatorPredicate { + private TestPredicate(String name, String value) { + super(name, value); + } + + @Override + public boolean match(String object) { + return false; + } + + @Override + public int getCost() { + return 0; + } + } + + private static TestPredicate f(final String name, final String value) { + return new TestPredicate(name, value); } public void testNotNot() { - final OperatorPredicate p = f("author", "bob"); - final Predicate n = p.not(); + final TestPredicate p = f("author", "bob"); + final Predicate n = not(p); assertTrue(n instanceof NotPredicate); assertNotSame(p, n); - assertSame(p, n.not()); + assertSame(p, not(n)); } public void testChildren() { - final OperatorPredicate p = f("author", "bob"); - final Predicate n = p.not(); + final TestPredicate p = f("author", "bob"); + final Predicate n = not(p); assertEquals(1, n.getChildCount()); assertSame(p, n.getChild(0)); } public void testChildrenUnmodifiable() { - final OperatorPredicate p = f("author", "bob"); - final Predicate n = p.not(); + final TestPredicate p = f("author", "bob"); + final Predicate n = not(p); try { n.getChildren().clear(); @@ -81,4 +101,30 @@ public class NotPredicateTest extends TestCase { assertTrue(not(f("a", "b")).hashCode() == not(f("a", "b")).hashCode()); assertFalse(not(f("a", "b")).hashCode() == not(f("a", "a")).hashCode()); } + + public void testCopy() { + final TestPredicate a = f("author", "alice"); + final TestPredicate b = f("author", "bob"); + final List sa = Collections.singletonList(a); + final List sb = Collections.singletonList(b); + final Predicate n = not(a); + + assertNotSame(n, n.copy(sa)); + assertEquals(sa, n.copy(sa).getChildren()); + + assertNotSame(n, n.copy(sb)); + assertEquals(sb, n.copy(sb).getChildren()); + + try { + n.copy(Collections. emptyList()); + } catch (IllegalArgumentException e) { + assertEquals("Expected exactly one child", e.getMessage()); + } + + try { + n.copy(and(a, b).getChildren()); + } catch (IllegalArgumentException e) { + assertEquals("Expected exactly one child", e.getMessage()); + } + } } diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/OrPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/OrPredicateTest.java new file mode 100644 index 0000000000..be820b92b9 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/OrPredicateTest.java @@ -0,0 +1,138 @@ +// Copyright (C) 2009 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.query; + +import static com.google.gerrit.server.query.Predicate.or; + +import junit.framework.TestCase; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class OrPredicateTest extends TestCase { + private static final class TestPredicate extends OperatorPredicate { + private TestPredicate(String name, String value) { + super(name, value); + } + + @Override + public boolean match(String object) { + return false; + } + + @Override + public int getCost() { + return 0; + } + } + + private static TestPredicate f(final String name, final String value) { + return new TestPredicate(name, value); + } + + public void testChildren() { + final TestPredicate a = f("author", "alice"); + final TestPredicate b = f("author", "bob"); + final Predicate n = or(a, b); + assertEquals(2, n.getChildCount()); + assertSame(a, n.getChild(0)); + assertSame(b, n.getChild(1)); + } + + public void testChildrenUnmodifiable() { + final TestPredicate a = f("author", "alice"); + final TestPredicate b = f("author", "bob"); + final Predicate n = or(a, b); + + try { + n.getChildren().clear(); + } catch (RuntimeException e) { + } + assertChildren("clear", n, list(a, b)); + + try { + n.getChildren().remove(0); + } catch (RuntimeException e) { + } + assertChildren("remove(0)", n, list(a, b)); + + try { + n.getChildren().iterator().remove(); + } catch (RuntimeException e) { + } + assertChildren("remove(0)", n, list(a, b)); + } + + private static void assertChildren(String o, Predicate p, + final List l) { + assertEquals(o + " did not affect child", l, p.getChildren()); + } + + public void testToString() { + final TestPredicate a = f("q", "alice"); + final TestPredicate b = f("q", "bob"); + final TestPredicate c = f("q", "charlie"); + assertEquals("(q:alice OR q:bob)", or(a, b).toString()); + assertEquals("(q:alice OR q:bob OR q:charlie)", or(a, b, c).toString()); + } + + public void testEquals() { + final TestPredicate a = f("author", "alice"); + final TestPredicate b = f("author", "bob"); + final TestPredicate c = f("author", "charlie"); + + assertTrue(or(a, b).equals(or(a, b))); + assertTrue(or(a, b, c).equals(or(a, b, c))); + + assertFalse(or(a, b).equals(or(b, a))); + assertFalse(or(a, c).equals(or(a, b))); + + assertFalse(or(a, c).equals(a)); + } + + public void testHashCode() { + final TestPredicate a = f("author", "alice"); + final TestPredicate b = f("author", "bob"); + final TestPredicate c = f("author", "charlie"); + + assertTrue(or(a, b).hashCode() == or(a, b).hashCode()); + assertTrue(or(a, b, c).hashCode() == or(a, b, c).hashCode()); + assertFalse(or(a, c).hashCode() == or(a, b).hashCode()); + } + + public void testCopy() { + final TestPredicate a = f("author", "alice"); + final TestPredicate b = f("author", "bob"); + final TestPredicate c = f("author", "charlie"); + final List s2 = list(a, b); + final List s3 = list(a, b, c); + final Predicate n2 = or(a, b); + + assertNotSame(n2, n2.copy(s2)); + assertEquals(s2, n2.copy(s2).getChildren()); + assertEquals(s3, n2.copy(s3).getChildren()); + + try { + n2.copy(Collections. emptyList()); + } catch (IllegalArgumentException e) { + assertEquals("Need at least two predicates", e.getMessage()); + } + } + + private static List list(final Predicate... predicates) { + return Arrays.asList(predicates); + } +}