Implement new /changes/{id}/action style REST API
All existing JSON APIs are converted to this new style. /changes/{id} parses the id field from a JSON response from a prior response and uses that to uniquely identify a change and verify the caller can see it. If the user requests only /changes/{id}/ then the data is returned as a single JSON object. This commit also gives full remote control of plugins using the /plugins/ namespace: PUT /plugins/{name} (JAR as request body) POST /plugins/{name} (JSON object {url:"https://..."}) DELETE /plugins/{name} GET /plugins/{name}/gerrit~status POST /plugins/{name}/gerrit~reload POST /plugins/{name}/gerrit~enable POST /plugins/{name}/gerrit~disable The commit provides some project admin commands: GET /projects/{name}/description PUT /projects/{name}/description GET /projects/{name}/parent PUT /projects/{name}/parent Project dashboards have moved: GET /projects/{name}/dashboards GET /projects/{name}/dashboards/{id} GET /projects/{name}/dashboards/default To access project names containing /, the name must be encoded with URL encoding, translating / to %2F. Change-Id: I6a38902ee473003ec637758b7c911f926a2e948a
This commit is contained in:
parent
401440f975
commit
ea6d0b5a27
@ -24,8 +24,8 @@ prefix the endpoint URL with `/a/`. For example to authenticate to
|
|||||||
[[output]]
|
[[output]]
|
||||||
Output Format
|
Output Format
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
Most APIs return text format by default. JSON can be requested
|
Most APIs return pretty printed JSON by default. Compact JSON can be
|
||||||
by setting the `Accept` HTTP request header to include
|
requested by setting the `Accept` HTTP request header to include
|
||||||
`application/json`, for example:
|
`application/json`, for example:
|
||||||
|
|
||||||
----
|
----
|
||||||
@ -43,12 +43,11 @@ body to a JSON parser:
|
|||||||
[ ... valid JSON ... ]
|
[ ... valid JSON ... ]
|
||||||
----
|
----
|
||||||
|
|
||||||
The default JSON format is `JSON_COMPACT`, which skips unnecessary
|
The default JSON format is pretty, which uses extra whitespace to make
|
||||||
whitespace. This is not the easiest format for a human to read. Many
|
the output more readable for a human. Producing (and parsing) the
|
||||||
examples in this documentation use `format=JSON` as a query parameter
|
non-pretty compact format is more efficient so tools should request it
|
||||||
to obtain pretty formatting in the response. Producing (and parsing)
|
by using the `Accept: application/json` header or `pp=0` query
|
||||||
the compact format is more efficient, so most tools should prefer the
|
parameter whenever possible.
|
||||||
default compact format.
|
|
||||||
|
|
||||||
Responses will be gzip compressed by the server if the HTTP
|
Responses will be gzip compressed by the server if the HTTP
|
||||||
`Accept-Encoding` request header is set to `gzip`. This may
|
`Accept-Encoding` request header is set to `gzip`. This may
|
||||||
@ -66,7 +65,7 @@ by UI tools to discover if administrative features are available
|
|||||||
to the caller, so they can hide (or show) relevant UI actions.
|
to the caller, so they can hide (or show) relevant UI actions.
|
||||||
|
|
||||||
----
|
----
|
||||||
GET /accounts/self/capabilities?format=JSON HTTP/1.0
|
GET /accounts/self/capabilities HTTP/1.0
|
||||||
|
|
||||||
)]}'
|
)]}'
|
||||||
{
|
{
|
||||||
@ -79,7 +78,7 @@ to the caller, so they can hide (or show) relevant UI actions.
|
|||||||
|
|
||||||
Administrator that has authenticated with digest authentication:
|
Administrator that has authenticated with digest authentication:
|
||||||
----
|
----
|
||||||
GET /a/accounts/self/capabilities?format=JSON HTTP/1.0
|
GET /a/accounts/self/capabilities HTTP/1.0
|
||||||
Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
|
Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
|
||||||
|
|
||||||
)]}'
|
)]}'
|
||||||
@ -106,7 +105,7 @@ Filtering may decrease the response time by avoiding looking at every
|
|||||||
possible alternative for the caller.
|
possible alternative for the caller.
|
||||||
|
|
||||||
----
|
----
|
||||||
GET /a/accounts/self/capabilities?format=JSON&q=createAccount&q=createGroup HTTP/1.0
|
GET /a/accounts/self/capabilities?q=createAccount&q=createGroup HTTP/1.0
|
||||||
Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
|
Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
|
||||||
|
|
||||||
)]}'
|
)]}'
|
||||||
@ -128,7 +127,7 @@ using the link:cmd-ls-projects.html[ls-projects] command over SSH,
|
|||||||
and accepts the same options as query parameters.
|
and accepts the same options as query parameters.
|
||||||
|
|
||||||
----
|
----
|
||||||
GET /projects/?format=JSON&d HTTP/1.0
|
GET /projects/?d HTTP/1.0
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 200 OK
|
||||||
Content-Disposition: attachment
|
Content-Disposition: attachment
|
||||||
@ -138,36 +137,112 @@ and accepts the same options as query parameters.
|
|||||||
{
|
{
|
||||||
"external/bison": {
|
"external/bison": {
|
||||||
"kind": "gerritcodereview#project",
|
"kind": "gerritcodereview#project",
|
||||||
|
"id": "external%2Fbison",
|
||||||
"description": "GNU parser generator"
|
"description": "GNU parser generator"
|
||||||
},
|
},
|
||||||
"external/gcc": {},
|
"external/gcc": {
|
||||||
|
"kind": "gerritcodereview#project",
|
||||||
|
"id": "external%2Fgcc",
|
||||||
|
},
|
||||||
"external/openssl": {
|
"external/openssl": {
|
||||||
|
"kind": "gerritcodereview#project",
|
||||||
|
"id": "external%2Fopenssl",
|
||||||
"description": "encryption\ncrypto routines"
|
"description": "encryption\ncrypto routines"
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
|
"kind": "gerritcodereview#project",
|
||||||
|
"id": "test",
|
||||||
"description": "\u003chtml\u003e is escaped"
|
"description": "\u003chtml\u003e is escaped"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
[[suggest-projects]]
|
[[suggest-projects]]
|
||||||
The `/projects/` URL also accepts a prefix string as part of the URL.
|
The `/projects/` URL also accepts a prefix string in the `p` parameter.
|
||||||
This limits the results to those projects that start with the specified
|
This limits the results to those projects that start with the specified
|
||||||
prefix.
|
prefix.
|
||||||
List all projects that start with `platform/`:
|
List all projects that start with `platform/`:
|
||||||
----
|
----
|
||||||
GET /projects/platform/?format=JSON HTTP/1.0
|
GET /projects/?p=platform%2F HTTP/1.0
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Disposition: attachment
|
HTTP/1.1 200 OK
|
||||||
Content-Type: application/json;charset=UTF-8
|
Content-Disposition: attachment
|
||||||
)]}'
|
Content-Type: application/json;charset=UTF-8
|
||||||
{
|
|
||||||
"platform/drivers": {},
|
)]}'
|
||||||
"platform/tools": {}
|
{
|
||||||
}
|
"platform/drivers": {
|
||||||
|
"kind": "gerritcodereview#project",
|
||||||
|
"id": "platform%2Fdrivers",
|
||||||
|
},
|
||||||
|
"platform/tools": {
|
||||||
|
"kind": "gerritcodereview#project",
|
||||||
|
"id": "platform%2Ftools",
|
||||||
|
}
|
||||||
|
}
|
||||||
----
|
----
|
||||||
E.g. this feature can be used by suggestion client UI's to limit results.
|
E.g. this feature can be used by suggestion client UI's to limit results.
|
||||||
|
|
||||||
|
/projects/*/dashboards/ (List Dashboards)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
List custom dashboards for a project.
|
||||||
|
|
||||||
|
The `/projects/{name}/dashboards/` URL expects the a URL encoded
|
||||||
|
project name as part of the URL. If name contains / the correct
|
||||||
|
encoding is to use `%2F`.
|
||||||
|
|
||||||
|
List all dashboards for the `work/my-project` project:
|
||||||
|
----
|
||||||
|
GET /projects/work%2Fmy-project/dashboards/ HTTP/1.0
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Disposition: attachment
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
)]}'
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"kind": "gerritcodereview#dashboard",
|
||||||
|
"id": "main:closed",
|
||||||
|
"ref": "main",
|
||||||
|
"path": "closed",
|
||||||
|
"description": "Merged and abandoned changes in last 7 weeks",
|
||||||
|
"url": "/dashboard/?title\u003dClosed+changes\u0026Merged\u003dstatus:merged+age:7w\u0026Abandoned\u003dstatus:abandoned+age:7w",
|
||||||
|
"default": true,
|
||||||
|
"title": "Closed changes",
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"name": "Merged",
|
||||||
|
"query": "status:merged age:7w"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Abandoned",
|
||||||
|
"query": "status:abandoned age:7w"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
----
|
||||||
|
|
||||||
|
To retrieve only the default dashboard, add `default` to the URL:
|
||||||
|
----
|
||||||
|
GET /projects/work%2Fmy-project/dashboards/default HTTP/1.0
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Disposition: attachment
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
)]}'
|
||||||
|
{
|
||||||
|
"kind": "gerritcodereview#dashboard",
|
||||||
|
"id": "main:closed",
|
||||||
|
"ref": "main",
|
||||||
|
"path": "closed",
|
||||||
|
"default": true,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
[[changes]]
|
[[changes]]
|
||||||
/changes/ (Query Changes)
|
/changes/ (Query Changes)
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
@ -177,7 +252,7 @@ the returned results.
|
|||||||
|
|
||||||
Query for open changes of watched projects:
|
Query for open changes of watched projects:
|
||||||
----
|
----
|
||||||
GET /changes/?format=JSON&q=status:open+is:watched&n=2 HTTP/1.0
|
GET /changes/q=status:open+is:watched&n=2 HTTP/1.0
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 200 OK
|
||||||
Content-Disposition: attachment
|
Content-Disposition: attachment
|
||||||
@ -237,7 +312,7 @@ arrays, one per query in the same order the queries were given in.
|
|||||||
|
|
||||||
Query that retrieves changes for a user's dashboard:
|
Query that retrieves changes for a user's dashboard:
|
||||||
----
|
----
|
||||||
GET /changes/?format=JSON&q=is:open+owner:self&q=is:open+reviewer:self+-owner:self&q=is:closed+owner:self+limit:5&o=LABELS HTTP/1.0
|
GET /changes/?q=is:open+owner:self&q=is:open+reviewer:self+-owner:self&q=is:closed+owner:self+limit:5&o=LABELS HTTP/1.0
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 200 OK
|
||||||
Content-Disposition: attachment
|
Content-Disposition: attachment
|
||||||
@ -304,7 +379,7 @@ default. Optional fields are:
|
|||||||
modified files will be output.
|
modified files will be output.
|
||||||
|
|
||||||
----
|
----
|
||||||
GET /changes/?q=97&format=JSON&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES HTTP/1.0
|
GET /changes/?q=97&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES HTTP/1.0
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
HTTP/1.1 200 OK
|
||||||
Content-Disposition: attachment
|
Content-Disposition: attachment
|
||||||
@ -396,60 +471,6 @@ default. Optional fields are:
|
|||||||
]
|
]
|
||||||
----
|
----
|
||||||
|
|
||||||
[[dashboards]]
|
|
||||||
/dashboards/project/ (List Dashboards)
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
Lists custom dashboards for a project.
|
|
||||||
|
|
||||||
The `/dashboards/project/` URL expects the project name as part of the
|
|
||||||
URL.
|
|
||||||
List all dashboards for the `myProject` project:
|
|
||||||
----
|
|
||||||
GET /dashboards/project/myProject?format=JSON&d HTTP/1.0
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Disposition: attachment
|
|
||||||
Content-Type: application/json;charset=UTF-8
|
|
||||||
|
|
||||||
)]}'
|
|
||||||
{
|
|
||||||
"refs/meta/dashboards/main:MyDashboard": {
|
|
||||||
"kind": "gerritcodereview#dashboard",
|
|
||||||
"id" : "refs/meta/dashboards/main:MyDashboard",
|
|
||||||
"dashboard_name": "MyDashboard",
|
|
||||||
"ref_name": "refs/meta/dashboards/main",
|
|
||||||
"project_name": "myProject",
|
|
||||||
"description": "Most recent open and merged changes.",
|
|
||||||
"parameters": "title\u003dMyDashboard\u0026Open+Changes\u003dstatus:open project:myProject limit:15\u0026Merged+Changes\u003dstatus:merged project:myProject limit:15",
|
|
||||||
"is_default": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
To retrieve only the default dashboard of a project set the parameter `default`.
|
|
||||||
|
|
||||||
----
|
|
||||||
GET /dashboards/project/myProject?default&format=JSON&d HTTP/1.0
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Disposition: attachment
|
|
||||||
Content-Type: application/json;charset=UTF-8
|
|
||||||
|
|
||||||
)]}'
|
|
||||||
{
|
|
||||||
"MyProject Dashboard": {
|
|
||||||
"kind": "gerritcodereview#dashboard",
|
|
||||||
"id" : "refs/meta/dashboards/main:MyDashboard",
|
|
||||||
"name": "MyDashboard",
|
|
||||||
"ref_name": "refs/meta/dashboards/main",
|
|
||||||
"project_name": "myProject",
|
|
||||||
"description": "Most recent open and merged changes.",
|
|
||||||
"parameters": "title\u003dMyDashboard\u0026Open+Changes\u003dstatus:open project:myProject limit:15\u0026Merged+Changes\u003dstatus:merged project:myProject limit:15",
|
|
||||||
"is_default": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
|
|
||||||
GERRIT
|
GERRIT
|
||||||
------
|
------
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.extensions.restapi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional interface for {@link RestCollection}.
|
||||||
|
* <p>
|
||||||
|
* Collections that implement this interface can accept a {@code PUT} or
|
||||||
|
* {@code POST} when the parse method throws {@link ResourceNotFoundException}.
|
||||||
|
*/
|
||||||
|
public interface AcceptsCreate<P extends RestResource> {
|
||||||
|
/**
|
||||||
|
* Handle creation of a child resource.
|
||||||
|
*
|
||||||
|
* @param parent parent collection handle.
|
||||||
|
* @param id id of the resource being created.
|
||||||
|
* @return a view to perform the creation. The create method must embed the id
|
||||||
|
* into the newly returned view object, as it will not be passed.
|
||||||
|
* @throws RestApiException the view cannot be constructed.
|
||||||
|
*/
|
||||||
|
<I> RestModifyView<P, I> create(P parent, String id) throws RestApiException;
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.extensions.restapi;
|
||||||
|
|
||||||
|
/** Caller cannot perform the request operation (HTTP 403 Forbidden). */
|
||||||
|
public class AuthException extends RestApiException {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** @param msg message to return to the client. */
|
||||||
|
public AuthException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.extensions.restapi;
|
||||||
|
|
||||||
|
/** Request could not be parsed as sent (HTTP 400 Bad Request). */
|
||||||
|
public class BadRequestException extends RestApiException {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** @param msg error text for client describing how request is bad. */
|
||||||
|
public BadRequestException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,174 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.extensions.restapi;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around a non-JSON result from a {@link RestView}.
|
||||||
|
* <p>
|
||||||
|
* Views may return this type to signal they want the server glue to write raw
|
||||||
|
* data to the client, instead of attempting automatic conversion to JSON. The
|
||||||
|
* create form is overloaded to handle plain text from a String, or binary data
|
||||||
|
* from a {@code byte[]} or {@code InputSteam}.
|
||||||
|
*/
|
||||||
|
public abstract class BinaryResult implements Closeable {
|
||||||
|
/** Default MIME type for unknown binary data. */
|
||||||
|
static final String OCTET_STREAM = "application/octet-stream";
|
||||||
|
|
||||||
|
/** Produce a UTF-8 encoded result from a string. */
|
||||||
|
public static BinaryResult create(String data) {
|
||||||
|
try {
|
||||||
|
return create(data.getBytes("UTF-8"))
|
||||||
|
.setContentType("text/plain")
|
||||||
|
.setCharacterEncoding("UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("JVM does not support UTF-8", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Produce an {@code application/octet-stream} result from a byte array. */
|
||||||
|
public static BinaryResult create(byte[] data) {
|
||||||
|
return new Array(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produce an {@code application/octet-stream} of unknown length by copying
|
||||||
|
* the InputStream until EOF. The server glue will automatically close this
|
||||||
|
* stream when copying is complete.
|
||||||
|
*/
|
||||||
|
public static BinaryResult create(InputStream data) {
|
||||||
|
return new Stream(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String contentType = OCTET_STREAM;
|
||||||
|
private String characterEncoding;
|
||||||
|
private long contentLength = -1;
|
||||||
|
private boolean gzip = true;
|
||||||
|
|
||||||
|
/** @return the MIME type of the result, for HTTP clients. */
|
||||||
|
public String getContentType() {
|
||||||
|
String enc = getCharacterEncoding();
|
||||||
|
if (enc != null) {
|
||||||
|
return contentType + "; charset=" + enc;
|
||||||
|
}
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set the MIME type of the result, and return {@code this}. */
|
||||||
|
public BinaryResult setContentType(String contentType) {
|
||||||
|
this.contentType = contentType != null ? contentType : OCTET_STREAM;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the character encoding; null if not known. */
|
||||||
|
public String getCharacterEncoding() {
|
||||||
|
return characterEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set the character set used to encode text data and return {@code this}. */
|
||||||
|
public BinaryResult setCharacterEncoding(String encoding) {
|
||||||
|
characterEncoding = encoding;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return length in bytes of the result; -1 if not known. */
|
||||||
|
public long getContentLength() {
|
||||||
|
return contentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set the content length of the result; -1 if not known. */
|
||||||
|
public BinaryResult setContentLength(long len) {
|
||||||
|
this.contentLength = len;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return true if this result can be gzip compressed to clients. */
|
||||||
|
public boolean canGzip() {
|
||||||
|
return gzip;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Disable gzip compression for already compressed responses. */
|
||||||
|
public BinaryResult disableGzip() {
|
||||||
|
this.gzip = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write or copy the result onto the specified output stream.
|
||||||
|
*
|
||||||
|
* @param os stream to write result data onto. This stream will be closed by
|
||||||
|
* the caller after this method returns.
|
||||||
|
* @throws IOException if the data cannot be produced, or the OutputStream
|
||||||
|
* {@code os} throws any IOException during a write or flush call.
|
||||||
|
*/
|
||||||
|
public abstract void writeTo(OutputStream os) throws IOException;
|
||||||
|
|
||||||
|
/** Close the result and release any resources it holds. */
|
||||||
|
public void close() throws IOException {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
if (getContentLength() >= 0) {
|
||||||
|
return String.format(
|
||||||
|
"BinaryResult[Content-Type: %s, Content-Length: %d]",
|
||||||
|
getContentType(), getContentLength());
|
||||||
|
}
|
||||||
|
return String.format(
|
||||||
|
"BinaryResult[Content-Type: %s, Content-Length: unknown]",
|
||||||
|
getContentType());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Array extends BinaryResult {
|
||||||
|
private final byte[] data;
|
||||||
|
|
||||||
|
Array(byte[] data) {
|
||||||
|
this.data = data;
|
||||||
|
setContentLength(data.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(OutputStream os) throws IOException {
|
||||||
|
os.write(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Stream extends BinaryResult {
|
||||||
|
private final InputStream src;
|
||||||
|
|
||||||
|
Stream(InputStream src) {
|
||||||
|
this.src = src;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(OutputStream dst) throws IOException {
|
||||||
|
byte[] tmp = new byte[4096];
|
||||||
|
int n;
|
||||||
|
while (0 < (n = src.read(tmp))) {
|
||||||
|
dst.write(tmp, 0, n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
src.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.extensions.restapi;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nested collection of {@link RestResource}s below a parent.
|
||||||
|
*
|
||||||
|
* @param <P> type of the parent resource.
|
||||||
|
* @param <C> type of resource operated on by each view.
|
||||||
|
*/
|
||||||
|
public interface ChildCollection<P extends RestResource, C extends RestResource>
|
||||||
|
extends RestView<P>, RestCollection<P, C> {
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.extensions.restapi;
|
||||||
|
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/** Applied to a String field to indicate the default input parameter. */
|
||||||
|
@Target({ElementType.FIELD})
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
public @interface DefaultInput {
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.extensions.restapi;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
|
||||||
|
/** Raw data stream supplied by the body of a PUT. */
|
||||||
|
public interface PutInput {
|
||||||
|
String getContentType();
|
||||||
|
long getContentLength();
|
||||||
|
InputStream getInputStream() throws IOException;
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.extensions.restapi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource state does not permit requested operation (HTTP 409 Conflict).
|
||||||
|
* <p>
|
||||||
|
* {@link RestModifyView} implementations may fail with this exception when the
|
||||||
|
* named resource does not permit the modification to take place at this time.
|
||||||
|
* An example use is trying to abandon a change that is already merged. The
|
||||||
|
* change cannot be abandoned once merged so an operation would throw.
|
||||||
|
*/
|
||||||
|
public class ResourceConflictException extends RestApiException {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** @param msg message to return to the client describing the error. */
|
||||||
|
public ResourceConflictException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.extensions.restapi;
|
||||||
|
|
||||||
|
/** Named resource does not exist (HTTP 404 Not Found). */
|
||||||
|
public class ResourceNotFoundException extends RestApiException {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** Requested resource is not found, failing portion not specified. */
|
||||||
|
public ResourceNotFoundException() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param id portion of the resource URI that does not exist. */
|
||||||
|
public ResourceNotFoundException(String id) {
|
||||||
|
super(id);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.extensions.restapi;
|
||||||
|
|
||||||
|
/** Root exception type for JSON API failures. */
|
||||||
|
public abstract class RestApiException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public RestApiException() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public RestApiException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RestApiException(String msg, Throwable cause) {
|
||||||
|
super(msg, cause);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,178 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.extensions.restapi;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.annotations.Export;
|
||||||
|
import com.google.gerrit.extensions.annotations.Exports;
|
||||||
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.Provider;
|
||||||
|
import com.google.inject.TypeLiteral;
|
||||||
|
import com.google.inject.binder.LinkedBindingBuilder;
|
||||||
|
import com.google.inject.binder.ScopedBindingBuilder;
|
||||||
|
|
||||||
|
/** Guice DSL for binding {@link RestView} implementations. */
|
||||||
|
public abstract class RestApiModule extends AbstractModule {
|
||||||
|
protected static final String GET = "GET";
|
||||||
|
protected static final String PUT = "PUT";
|
||||||
|
protected static final String DELETE = "DELETE";
|
||||||
|
protected static final String POST = "POST";
|
||||||
|
|
||||||
|
protected <R extends RestResource>
|
||||||
|
ReadViewBinder<R> get(TypeLiteral<RestView<R>> viewType) {
|
||||||
|
return new ReadViewBinder<R>(view(viewType, GET, "/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <R extends RestResource>
|
||||||
|
ModifyViewBinder<R> put(TypeLiteral<RestView<R>> viewType) {
|
||||||
|
return new ModifyViewBinder<R>(view(viewType, PUT, "/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <R extends RestResource>
|
||||||
|
ModifyViewBinder<R> post(TypeLiteral<RestView<R>> viewType) {
|
||||||
|
return new ModifyViewBinder<R>(view(viewType, POST, "/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <R extends RestResource>
|
||||||
|
ModifyViewBinder<R> delete(TypeLiteral<RestView<R>> viewType) {
|
||||||
|
return new ModifyViewBinder<R>(view(viewType, DELETE, "/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <R extends RestResource>
|
||||||
|
ReadViewBinder<R> get(TypeLiteral<RestView<R>> viewType, String name) {
|
||||||
|
return new ReadViewBinder<R>(view(viewType, GET, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <R extends RestResource>
|
||||||
|
ModifyViewBinder<R> put(TypeLiteral<RestView<R>> viewType, String name) {
|
||||||
|
return new ModifyViewBinder<R>(view(viewType, PUT, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <R extends RestResource>
|
||||||
|
ModifyViewBinder<R> post(TypeLiteral<RestView<R>> viewType, String name) {
|
||||||
|
return new ModifyViewBinder<R>(view(viewType, POST, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <R extends RestResource>
|
||||||
|
ModifyViewBinder<R> delete(TypeLiteral<RestView<R>> viewType, String name) {
|
||||||
|
return new ModifyViewBinder<R>(view(viewType, DELETE, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <P extends RestResource>
|
||||||
|
ChildCollectionBinder<P> child(TypeLiteral<RestView<P>> type, String name) {
|
||||||
|
return new ChildCollectionBinder<P>(view(type, GET, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <R extends RestResource>
|
||||||
|
LinkedBindingBuilder<RestView<R>> view(
|
||||||
|
TypeLiteral<RestView<R>> viewType,
|
||||||
|
String method,
|
||||||
|
String name) {
|
||||||
|
return bind(viewType).annotatedWith(export(method, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Export export(String method, String name) {
|
||||||
|
if (name.length() > 1 && name.startsWith("/")) {
|
||||||
|
// Views may be bound as "/" to mean the resource itself, or
|
||||||
|
// as "status" as in "/type/{id}/status". Don't bind "/status"
|
||||||
|
// if the caller asked for that, bind what the server expects.
|
||||||
|
name = name.substring(1);
|
||||||
|
}
|
||||||
|
return Exports.named(method + "." + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ReadViewBinder<P extends RestResource> {
|
||||||
|
private final LinkedBindingBuilder<RestView<P>> binder;
|
||||||
|
|
||||||
|
private ReadViewBinder(LinkedBindingBuilder<RestView<P>> binder) {
|
||||||
|
this.binder = binder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends RestReadView<P>>
|
||||||
|
ScopedBindingBuilder to(Class<T> impl) {
|
||||||
|
return binder.to(impl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends RestReadView<P>>
|
||||||
|
void toInstance(T impl) {
|
||||||
|
binder.toInstance(impl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends RestReadView<P>>
|
||||||
|
ScopedBindingBuilder toProvider(Class<? extends Provider<? extends T>> providerType) {
|
||||||
|
return binder.toProvider(providerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends RestReadView<P>>
|
||||||
|
ScopedBindingBuilder toProvider(Provider<? extends T> provider) {
|
||||||
|
return binder.toProvider(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ModifyViewBinder<P extends RestResource> {
|
||||||
|
private final LinkedBindingBuilder<RestView<P>> binder;
|
||||||
|
|
||||||
|
private ModifyViewBinder(LinkedBindingBuilder<RestView<P>> binder) {
|
||||||
|
this.binder = binder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends RestModifyView<P, ?>>
|
||||||
|
ScopedBindingBuilder to(Class<T> impl) {
|
||||||
|
return binder.to(impl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends RestModifyView<P, ?>>
|
||||||
|
void toInstance(T impl) {
|
||||||
|
binder.toInstance(impl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends RestModifyView<P, ?>>
|
||||||
|
ScopedBindingBuilder toProvider(Class<? extends Provider<? extends T>> providerType) {
|
||||||
|
return binder.toProvider(providerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends RestModifyView<P, ?>>
|
||||||
|
ScopedBindingBuilder toProvider(Provider<? extends T> provider) {
|
||||||
|
return binder.toProvider(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ChildCollectionBinder<P extends RestResource> {
|
||||||
|
private final LinkedBindingBuilder<RestView<P>> binder;
|
||||||
|
|
||||||
|
private ChildCollectionBinder(LinkedBindingBuilder<RestView<P>> binder) {
|
||||||
|
this.binder = binder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <C extends RestResource, T extends ChildCollection<P, C>>
|
||||||
|
ScopedBindingBuilder to(Class<T> impl) {
|
||||||
|
return binder.to(impl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <C extends RestResource, T extends ChildCollection<P, C>>
|
||||||
|
void toInstance(T impl) {
|
||||||
|
binder.toInstance(impl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <C extends RestResource, T extends ChildCollection<P, C>>
|
||||||
|
ScopedBindingBuilder toProvider(Class<? extends Provider<? extends T>> providerType) {
|
||||||
|
return binder.toProvider(providerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <C extends RestResource, T extends ChildCollection<P, C>>
|
||||||
|
ScopedBindingBuilder toProvider(Provider<? extends T> provider) {
|
||||||
|
return binder.toProvider(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.extensions.restapi;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of resources accessible through a REST API.
|
||||||
|
* <p>
|
||||||
|
* To build a collection declare a resource, the map in a module, and the
|
||||||
|
* collection itself accepting the map:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* public class MyResource implements RestResource {
|
||||||
|
* public static final TypeLiteral<RestView<MyResource>> MY_KIND =
|
||||||
|
* new TypeLiteral<RestView<MyResource>>() {};
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* public class MyModule extends AbstractModule {
|
||||||
|
* @Override
|
||||||
|
* protected void configure() {
|
||||||
|
* DynamicMap.mapOf(binder(), MyResource.MY_KIND);
|
||||||
|
*
|
||||||
|
* get(MyResource.MY_KIND, "action").to(MyAction.class);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* public class MyCollection extends RestCollection<TopLevelResource, MyResource> {
|
||||||
|
* private final DynamicMap<RestView<MyResource>> views;
|
||||||
|
*
|
||||||
|
* @Inject
|
||||||
|
* MyCollection(DynamicMap<RestView<MyResource>> views) {
|
||||||
|
* this.views = views;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* public DynamicMap<RestView<MyResource>> views() {
|
||||||
|
* return views;
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* To build a nested collection, implement {@link ChildCollection}.
|
||||||
|
*
|
||||||
|
* @param <P> type of the parent resource. For a top level collection this
|
||||||
|
* should always be {@link TopLevelResource}.
|
||||||
|
* @param <R> type of resource operated on by each view.
|
||||||
|
*/
|
||||||
|
public interface RestCollection<P extends RestResource, R extends RestResource> {
|
||||||
|
/**
|
||||||
|
* Create a view to list the contents of the collection.
|
||||||
|
* <p>
|
||||||
|
* The returned view should accept the parent type to scope the search, and
|
||||||
|
* may want to take a "q" parameter option to narrow the results.
|
||||||
|
*
|
||||||
|
* @return view to list the collection.
|
||||||
|
* @throws ResourceNotFoundException if the collection cannot be listed.
|
||||||
|
*/
|
||||||
|
RestView<P> list() throws ResourceNotFoundException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a path component into a resource handle.
|
||||||
|
*
|
||||||
|
* @param parent the handle to the collection.
|
||||||
|
* @param id string identifier supplied by the client. In a URL such as
|
||||||
|
* {@code /changes/1234/abandon} this string is {@code "1234"}.
|
||||||
|
* @return a resource handle for the identified object.
|
||||||
|
* @throws ResourceNotFoundException the object does not exist, or the caller
|
||||||
|
* is not permitted to know if the resource exists.
|
||||||
|
* @throws Exception if the implementation had any errors converting to a
|
||||||
|
* resource handle. This results in an HTTP 500 Internal Server Error.
|
||||||
|
*/
|
||||||
|
R parse(P parent, String id) throws ResourceNotFoundException, Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the views that support this collection.
|
||||||
|
* <p>
|
||||||
|
* Within a resource the views are accessed as {@code RESOURCE/plugin~view}.
|
||||||
|
*
|
||||||
|
* @return map of views.
|
||||||
|
*/
|
||||||
|
DynamicMap<RestView<R>> views();
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.extensions.restapi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RestView that supports accepting input and changing a resource.
|
||||||
|
* <p>
|
||||||
|
* The input must be supplied as JSON as the body of the HTTP request. Modify
|
||||||
|
* views can be invoked by any HTTP method that is not {@code GET}, which
|
||||||
|
* includes {@code POST}, {@code PUT}, {@code DELETE}.
|
||||||
|
*
|
||||||
|
* @param <R> type of the resource the view modifies.
|
||||||
|
* @param <I> type of input the JSON parser will parse the input into.
|
||||||
|
*/
|
||||||
|
public interface RestModifyView<R extends RestResource, I> extends RestView<R> {
|
||||||
|
/**
|
||||||
|
* @return Java class object defining the input type. The JSON parser will
|
||||||
|
* parse the supplied request body into a new instance of this class
|
||||||
|
* before passing it to apply.
|
||||||
|
*/
|
||||||
|
Class<I> inputType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the view operation by altering the resource.
|
||||||
|
*
|
||||||
|
* @param resource resource to modify.
|
||||||
|
* @param input input after parsing from request.
|
||||||
|
* @return result to return to the client. Use {@link BinaryResult} to avoid
|
||||||
|
* automatic conversion to JSON.
|
||||||
|
* @throws AuthException the client is not permitted to access this view.
|
||||||
|
* @throws BadRequestException the request was incorrectly specified and
|
||||||
|
* cannot be handled by this view.
|
||||||
|
* @throws ResourceConflictException the resource state does not permit this
|
||||||
|
* view to make the changes at this time.
|
||||||
|
* @throws Exception the implementation of the view failed. The exception will
|
||||||
|
* be logged and HTTP 500 Internal Server Error will be returned to
|
||||||
|
* the client.
|
||||||
|
*/
|
||||||
|
Object apply(R resource, I input) throws AuthException, BadRequestException,
|
||||||
|
ResourceConflictException, Exception;
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.extensions.restapi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RestView to read a resource without modification.
|
||||||
|
*
|
||||||
|
* @param <R> type of resource the view reads.
|
||||||
|
*/
|
||||||
|
public interface RestReadView<R extends RestResource> extends RestView<R> {
|
||||||
|
/**
|
||||||
|
* Process the view operation by reading from the resource.
|
||||||
|
*
|
||||||
|
* @param resource resource to modify.
|
||||||
|
* @return result to return to the client. Use {@link BinaryResult} to avoid
|
||||||
|
* automatic conversion to JSON.
|
||||||
|
* @throws AuthException the client is not permitted to access this view.
|
||||||
|
* @throws BadRequestException the request was incorrectly specified and
|
||||||
|
* cannot be handled by this view.
|
||||||
|
* @throws ResourceConflictException the resource state does not permit this
|
||||||
|
* view to make the changes at this time.
|
||||||
|
* @throws Exception the implementation of the view failed. The exception will
|
||||||
|
* be logged and HTTP 500 Internal Server Error will be returned to
|
||||||
|
* the client.
|
||||||
|
*/
|
||||||
|
Object apply(R resource) throws AuthException, BadRequestException,
|
||||||
|
ResourceConflictException, Exception;
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.extensions.restapi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic resource handle defining arguments to views.
|
||||||
|
* <p>
|
||||||
|
* Resource handle returned by {@link RestCollection} and passed to a
|
||||||
|
* {@link RestView} such as {@link RestReadView} or {@link RestModifyView}.
|
||||||
|
*/
|
||||||
|
public interface RestResource {
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.extensions.restapi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any type of view, see {@link RestReadView} for reads, {@link RestModifyView}
|
||||||
|
* for updates, and {@link RestCollection} for nested collections.
|
||||||
|
*/
|
||||||
|
public interface RestView<R extends RestResource> {
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (thte "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.extensions.restapi;
|
||||||
|
|
||||||
|
/** Special marker resource naming the top-level of a REST space. */
|
||||||
|
public class TopLevelResource implements RestResource {
|
||||||
|
public static final TopLevelResource INSTANCE = new TopLevelResource();
|
||||||
|
|
||||||
|
private TopLevelResource() {
|
||||||
|
}
|
||||||
|
}
|
@ -14,11 +14,11 @@
|
|||||||
|
|
||||||
package com.google.gerrit.client.admin;
|
package com.google.gerrit.client.admin;
|
||||||
|
|
||||||
import com.google.gerrit.client.dashboards.DashboardMap;
|
import com.google.gerrit.client.dashboards.DashboardList;
|
||||||
import com.google.gerrit.client.dashboards.DashboardsTable;
|
import com.google.gerrit.client.dashboards.DashboardsTable;
|
||||||
|
import com.google.gerrit.client.rpc.NativeList;
|
||||||
import com.google.gerrit.client.rpc.ScreenLoadCallback;
|
import com.google.gerrit.client.rpc.ScreenLoadCallback;
|
||||||
import com.google.gerrit.reviewdb.client.Project;
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
|
|
||||||
import com.google.gwt.user.client.ui.FlowPanel;
|
import com.google.gwt.user.client.ui.FlowPanel;
|
||||||
|
|
||||||
public class ProjectDashboardsScreen extends ProjectScreen {
|
public class ProjectDashboardsScreen extends ProjectScreen {
|
||||||
@ -33,10 +33,10 @@ public class ProjectDashboardsScreen extends ProjectScreen {
|
|||||||
@Override
|
@Override
|
||||||
protected void onLoad() {
|
protected void onLoad() {
|
||||||
super.onLoad();
|
super.onLoad();
|
||||||
DashboardMap.allOnProject(getProjectKey(),
|
DashboardList.all(getProjectKey(),
|
||||||
new ScreenLoadCallback<DashboardMap>(this) {
|
new ScreenLoadCallback<NativeList<DashboardList>>(this) {
|
||||||
@Override
|
@Override
|
||||||
protected void preDisplay(final DashboardMap result) {
|
protected void preDisplay(NativeList<DashboardList> result) {
|
||||||
dashes.display(result);
|
dashes.display(result);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -18,13 +18,12 @@ import com.google.gwt.core.client.JavaScriptObject;
|
|||||||
|
|
||||||
public class DashboardInfo extends JavaScriptObject {
|
public class DashboardInfo extends JavaScriptObject {
|
||||||
public final native String id() /*-{ return this.id; }-*/;
|
public final native String id() /*-{ return this.id; }-*/;
|
||||||
public final native String name() /*-{ return this.dashboard_name; }-*/;
|
public final native String project() /*-{ return this.project; }-*/;
|
||||||
public final native String section() /*-{ return this.section; }-*/;
|
public final native String ref() /*-{ return this.ref; }-*/;
|
||||||
public final native String refName() /*-{ return this.ref_name; }-*/;
|
public final native String path() /*-{ return this.path; }-*/;
|
||||||
public final native String projectName() /*-{ return this.project_name; }-*/;
|
|
||||||
public final native String description() /*-{ return this.description; }-*/;
|
public final native String description() /*-{ return this.description; }-*/;
|
||||||
public final native String parameters() /*-{ return this.parameters; }-*/;
|
public final native String url() /*-{ return this.url; }-*/;
|
||||||
public final native boolean isDefault() /*-{ return this.is_default ? true : false; }-*/;
|
public final native boolean isDefault() /*-{ return this['default'] ? true : false; }-*/;
|
||||||
|
|
||||||
protected DashboardInfo() {
|
protected DashboardInfo() {
|
||||||
}
|
}
|
||||||
|
@ -14,27 +14,31 @@
|
|||||||
|
|
||||||
package com.google.gerrit.client.dashboards;
|
package com.google.gerrit.client.dashboards;
|
||||||
|
|
||||||
import com.google.gerrit.client.rpc.NativeMap;
|
import com.google.gerrit.client.rpc.NativeList;
|
||||||
import com.google.gerrit.client.rpc.RestApi;
|
import com.google.gerrit.client.rpc.RestApi;
|
||||||
import com.google.gerrit.reviewdb.client.Project;
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
import com.google.gwtjsonrpc.common.AsyncCallback;
|
|
||||||
import com.google.gwt.http.client.URL;
|
import com.google.gwt.http.client.URL;
|
||||||
|
import com.google.gwtjsonrpc.common.AsyncCallback;
|
||||||
|
|
||||||
/** Dashboards available from {@code /dashboards/}. */
|
/** Project dashboards from {@code /projects/<name>/dashboards/}. */
|
||||||
public class DashboardMap extends NativeMap<DashboardInfo> {
|
public class DashboardList extends NativeList<DashboardInfo> {
|
||||||
public static void allOnProject(Project.NameKey project,
|
public static void all(Project.NameKey project,
|
||||||
AsyncCallback<DashboardMap> callback) {
|
AsyncCallback<NativeList<DashboardList>> callback) {
|
||||||
new RestApi("/dashboards/project/" + URL.encode(project.get()).replaceAll("[?]", "%3F"))
|
new RestApi(base(project))
|
||||||
.get(NativeMap.copyKeysIntoChildren(callback));
|
.addParameterTrue("inherited")
|
||||||
|
.get(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void projectDefault(Project.NameKey project,
|
public static void defaultDashboard(Project.NameKey project,
|
||||||
AsyncCallback<DashboardMap> callback) {
|
AsyncCallback<DashboardInfo> callback) {
|
||||||
new RestApi("/dashboards/project/" + URL.encode(project.get()).replaceAll("[?]", "%3F"))
|
new RestApi(base(project) + "default").get(callback);
|
||||||
.addParameterTrue("default")
|
|
||||||
.get(NativeMap.copyKeysIntoChildren(callback));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected DashboardMap() {
|
private static String base(Project.NameKey project) {
|
||||||
|
String name = URL.encodePathSegment(project.get());
|
||||||
|
return "/projects/" + name + "/dashboards/";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected DashboardList() {
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -15,6 +15,7 @@
|
|||||||
package com.google.gerrit.client.dashboards;
|
package com.google.gerrit.client.dashboards;
|
||||||
|
|
||||||
import com.google.gerrit.client.Gerrit;
|
import com.google.gerrit.client.Gerrit;
|
||||||
|
import com.google.gerrit.client.rpc.NativeList;
|
||||||
import com.google.gerrit.client.ui.NavigationTable;
|
import com.google.gerrit.client.ui.NavigationTable;
|
||||||
import com.google.gerrit.reviewdb.client.Project;
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
import com.google.gwt.user.client.History;
|
import com.google.gwt.user.client.History;
|
||||||
@ -22,9 +23,12 @@ import com.google.gwt.user.client.ui.Anchor;
|
|||||||
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
|
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
|
||||||
import com.google.gwt.user.client.ui.Image;
|
import com.google.gwt.user.client.ui.Image;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class DashboardsTable extends NavigationTable<DashboardInfo> {
|
public class DashboardsTable extends NavigationTable<DashboardInfo> {
|
||||||
Project.NameKey project;
|
Project.NameKey project;
|
||||||
@ -47,12 +51,27 @@ public class DashboardsTable extends NavigationTable<DashboardInfo> {
|
|||||||
table.setText(0, 3, Util.C.dashboardInherited());
|
table.setText(0, 3, Util.C.dashboardInherited());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void display(DashboardMap dashes) {
|
public void display(DashboardList dashes) {
|
||||||
|
display(dashes.asList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void display(NativeList<DashboardList> in) {
|
||||||
|
Map<String, DashboardInfo> map = new HashMap<String, DashboardInfo>();
|
||||||
|
for (DashboardList list : in.asList()) {
|
||||||
|
for (DashboardInfo d : list.asList()) {
|
||||||
|
if (!map.containsKey(d.id())) {
|
||||||
|
map.put(d.id(), d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
display(new ArrayList<DashboardInfo>(map.values()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void display(List<DashboardInfo> list) {
|
||||||
while (1 < table.getRowCount()) {
|
while (1 < table.getRowCount()) {
|
||||||
table.removeRow(table.getRowCount() - 1);
|
table.removeRow(table.getRowCount() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DashboardInfo> list = dashes.values().asList();
|
|
||||||
Collections.sort(list, new Comparator<DashboardInfo>() {
|
Collections.sort(list, new Comparator<DashboardInfo>() {
|
||||||
@Override
|
@Override
|
||||||
public int compare(DashboardInfo a, DashboardInfo b) {
|
public int compare(DashboardInfo a, DashboardInfo b) {
|
||||||
@ -60,11 +79,11 @@ public class DashboardsTable extends NavigationTable<DashboardInfo> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
String section = null;
|
String ref = null;
|
||||||
for(DashboardInfo d : list) {
|
for(DashboardInfo d : list) {
|
||||||
if (!d.section().equals(section)) {
|
if (!d.ref().equals(ref)) {
|
||||||
section = d.section();
|
ref = d.ref();
|
||||||
insertTitleRow(table.getRowCount(), section);
|
insertTitleRow(table.getRowCount(), ref);
|
||||||
}
|
}
|
||||||
insert(table.getRowCount(), d);
|
insert(table.getRowCount(), d);
|
||||||
}
|
}
|
||||||
@ -102,18 +121,17 @@ public class DashboardsTable extends NavigationTable<DashboardInfo> {
|
|||||||
final FlexCellFormatter fmt = table.getFlexCellFormatter();
|
final FlexCellFormatter fmt = table.getFlexCellFormatter();
|
||||||
fmt.getElement(row, 1).setTitle(Util.C.dashboardDefaultToolTip());
|
fmt.getElement(row, 1).setTitle(Util.C.dashboardDefaultToolTip());
|
||||||
}
|
}
|
||||||
table.setWidget(row, 2, new Anchor(k.name(), "#" + link(k)));
|
table.setWidget(row, 2, new Anchor(k.path(), "#" + k.url()));
|
||||||
table.setText(row, 3, k.description());
|
table.setText(row, 3, k.description());
|
||||||
if (!project.get().equals(k.projectName())) {
|
if (k.project() != null && !project.get().equals(k.project())) {
|
||||||
table.setText(row, 4, k.projectName());
|
table.setText(row, 4, k.project());
|
||||||
}
|
}
|
||||||
|
|
||||||
setRowItem(row, k);
|
setRowItem(row, k);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Object getRowItemKey(final DashboardInfo item) {
|
protected Object getRowItemKey(final DashboardInfo item) {
|
||||||
return item.name();
|
return item.id();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -121,10 +139,6 @@ public class DashboardsTable extends NavigationTable<DashboardInfo> {
|
|||||||
if (row > 0) {
|
if (row > 0) {
|
||||||
movePointerTo(row);
|
movePointerTo(row);
|
||||||
}
|
}
|
||||||
History.newItem(link(getRowItem(row)));
|
History.newItem(getRowItem(row).url());
|
||||||
}
|
|
||||||
|
|
||||||
private String link(final DashboardInfo item) {
|
|
||||||
return "/dashboard/?" + item.parameters();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@ package com.google.gerrit.client.projects;
|
|||||||
import com.google.gerrit.client.rpc.NativeMap;
|
import com.google.gerrit.client.rpc.NativeMap;
|
||||||
import com.google.gerrit.client.rpc.RestApi;
|
import com.google.gerrit.client.rpc.RestApi;
|
||||||
import com.google.gwtjsonrpc.common.AsyncCallback;
|
import com.google.gwtjsonrpc.common.AsyncCallback;
|
||||||
import com.google.gwt.http.client.URL;
|
|
||||||
|
|
||||||
/** Projects available from {@code /projects/}. */
|
/** Projects available from {@code /projects/}. */
|
||||||
public class ProjectMap extends NativeMap<ProjectInfo> {
|
public class ProjectMap extends NativeMap<ProjectInfo> {
|
||||||
@ -46,9 +45,10 @@ public class ProjectMap extends NativeMap<ProjectInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void suggest(String prefix, int limit, AsyncCallback<ProjectMap> cb) {
|
public static void suggest(String prefix, int limit, AsyncCallback<ProjectMap> cb) {
|
||||||
new RestApi("/projects/" + URL.encode(prefix).replaceAll("[?]", "%3F"))
|
new RestApi("/projects/")
|
||||||
.addParameterRaw("type", "ALL")
|
.addParameter("p", prefix)
|
||||||
.addParameter("n", limit)
|
.addParameter("n", limit)
|
||||||
|
.addParameterRaw("type", "ALL")
|
||||||
.addParameterTrue("d") // description
|
.addParameterTrue("d") // description
|
||||||
.get(NativeMap.copyKeysIntoChildren(cb));
|
.get(NativeMap.copyKeysIntoChildren(cb));
|
||||||
}
|
}
|
||||||
|
@ -1,232 +0,0 @@
|
|||||||
// Copyright (C) 2012 The Android Open Source Project
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package com.google.gerrit.httpd;
|
|
||||||
|
|
||||||
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
|
||||||
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
|
||||||
|
|
||||||
import com.google.common.base.Objects;
|
|
||||||
import com.google.common.base.Strings;
|
|
||||||
import com.google.gerrit.extensions.annotations.RequiresCapability;
|
|
||||||
import com.google.gerrit.server.CurrentUser;
|
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
|
||||||
import com.google.gerrit.server.account.CapabilityControl;
|
|
||||||
import com.google.gerrit.util.cli.CmdLineParser;
|
|
||||||
import com.google.gwtjsonrpc.common.JsonConstants;
|
|
||||||
import com.google.gwtjsonrpc.server.RPCServletUtils;
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import com.google.inject.Provider;
|
|
||||||
|
|
||||||
import org.kohsuke.args4j.CmdLineException;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.StringWriter;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.servlet.http.HttpServlet;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
public abstract class RestApiServlet extends HttpServlet {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
private static final Logger log =
|
|
||||||
LoggerFactory.getLogger(RestApiServlet.class);
|
|
||||||
|
|
||||||
/** MIME type used for a JSON response body. */
|
|
||||||
protected static final String JSON_TYPE = JsonConstants.JSON_TYPE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Garbage prefix inserted before JSON output to prevent XSSI.
|
|
||||||
* <p>
|
|
||||||
* This prefix is ")]}'\n" and is designed to prevent a web browser from
|
|
||||||
* executing the response body if the resource URI were to be referenced using
|
|
||||||
* a <script src="...> HTML tag from another web site. Clients using the
|
|
||||||
* HTTP interface will need to always strip the first line of response data to
|
|
||||||
* remove this magic header.
|
|
||||||
*/
|
|
||||||
protected static final byte[] JSON_MAGIC;
|
|
||||||
|
|
||||||
static {
|
|
||||||
try {
|
|
||||||
JSON_MAGIC = ")]}'\n".getBytes("UTF-8");
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
throw new RuntimeException("UTF-8 not supported", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Provider<CurrentUser> currentUser;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
protected RestApiServlet(final Provider<CurrentUser> currentUser) {
|
|
||||||
this.currentUser = currentUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void service(HttpServletRequest req, HttpServletResponse res)
|
|
||||||
throws ServletException, IOException {
|
|
||||||
res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
|
|
||||||
res.setHeader("Pragma", "no-cache");
|
|
||||||
res.setHeader("Cache-Control", "no-cache, must-revalidate");
|
|
||||||
res.setHeader("Content-Disposition", "attachment");
|
|
||||||
|
|
||||||
try {
|
|
||||||
checkRequiresCapability();
|
|
||||||
super.service(req, res);
|
|
||||||
} catch (RequireCapabilityException err) {
|
|
||||||
sendError(res, SC_FORBIDDEN, err.getMessage());
|
|
||||||
} catch (Error err) {
|
|
||||||
handleException(err, req, res);
|
|
||||||
} catch (RuntimeException err) {
|
|
||||||
handleException(err, req, res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkRequiresCapability() throws RequireCapabilityException {
|
|
||||||
RequiresCapability rc = getClass().getAnnotation(RequiresCapability.class);
|
|
||||||
if (rc != null) {
|
|
||||||
CurrentUser user = currentUser.get();
|
|
||||||
CapabilityControl ctl = user.getCapabilities();
|
|
||||||
if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
|
|
||||||
String msg = String.format(
|
|
||||||
"fatal: %s does not have \"%s\" capability.",
|
|
||||||
Objects.firstNonNull(
|
|
||||||
user.getUserName(),
|
|
||||||
user instanceof IdentifiedUser
|
|
||||||
? ((IdentifiedUser) user).getNameEmail()
|
|
||||||
: user.toString()),
|
|
||||||
rc.value());
|
|
||||||
throw new RequireCapabilityException(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void handleException(Throwable err, HttpServletRequest req,
|
|
||||||
HttpServletResponse res) throws IOException {
|
|
||||||
String uri = req.getRequestURI();
|
|
||||||
if (!Strings.isNullOrEmpty(req.getQueryString())) {
|
|
||||||
uri += "?" + req.getQueryString();
|
|
||||||
}
|
|
||||||
log.error(String.format("Error in %s %s", req.getMethod(), uri), err);
|
|
||||||
|
|
||||||
if (!res.isCommitted()) {
|
|
||||||
res.reset();
|
|
||||||
sendError(res, SC_INTERNAL_SERVER_ERROR, "Internal Server Error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static void sendError(HttpServletResponse res,
|
|
||||||
int statusCode, String msg) throws IOException {
|
|
||||||
res.setStatus(statusCode);
|
|
||||||
sendText(null, res, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static boolean acceptsJson(HttpServletRequest req) {
|
|
||||||
String accept = req.getHeader("Accept");
|
|
||||||
if (accept == null) {
|
|
||||||
return false;
|
|
||||||
} else if (JSON_TYPE.equals(accept)) {
|
|
||||||
return true;
|
|
||||||
} else if (accept.startsWith(JSON_TYPE + ",")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for (String p : accept.split("[ ,;][ ,;]*")) {
|
|
||||||
if (JSON_TYPE.equals(p)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static void sendText(@Nullable HttpServletRequest req,
|
|
||||||
HttpServletResponse res, String data) throws IOException {
|
|
||||||
res.setContentType("text/plain");
|
|
||||||
res.setCharacterEncoding("UTF-8");
|
|
||||||
send(req, res, data.getBytes("UTF-8"));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static void send(@Nullable HttpServletRequest req,
|
|
||||||
HttpServletResponse res, byte[] data) throws IOException {
|
|
||||||
if (data.length > 256 && req != null
|
|
||||||
&& RPCServletUtils.acceptsGzipEncoding(req)) {
|
|
||||||
res.setHeader("Content-Encoding", "gzip");
|
|
||||||
data = HtmlDomUtil.compress(data);
|
|
||||||
}
|
|
||||||
res.setContentLength(data.length);
|
|
||||||
OutputStream out = res.getOutputStream();
|
|
||||||
try {
|
|
||||||
out.write(data);
|
|
||||||
} finally {
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ParameterParser {
|
|
||||||
private final CmdLineParser.Factory parserFactory;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
ParameterParser(CmdLineParser.Factory pf) {
|
|
||||||
this.parserFactory = pf;
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> boolean parse(T param, HttpServletRequest req,
|
|
||||||
HttpServletResponse res) throws IOException {
|
|
||||||
return parse(param, req, res, Collections.<String>emptySet());
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> boolean parse(T param, HttpServletRequest req,
|
|
||||||
HttpServletResponse res, Set<String> argNames) throws IOException {
|
|
||||||
CmdLineParser clp = parserFactory.create(param);
|
|
||||||
try {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, String[]> parameterMap = req.getParameterMap();
|
|
||||||
clp.parseOptionMap(parameterMap, argNames);
|
|
||||||
} catch (CmdLineException e) {
|
|
||||||
if (!clp.wasHelpRequestedByOption()) {
|
|
||||||
res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
|
||||||
sendText(req, res, e.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clp.wasHelpRequestedByOption()) {
|
|
||||||
StringWriter msg = new StringWriter();
|
|
||||||
clp.printQueryStringUsage(req.getRequestURI(), msg);
|
|
||||||
msg.write('\n');
|
|
||||||
msg.write('\n');
|
|
||||||
clp.printUsage(msg, null);
|
|
||||||
msg.write('\n');
|
|
||||||
sendText(req, res, msg.toString());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("serial") // Never serialized or thrown out of this class.
|
|
||||||
private static class RequireCapabilityException extends Exception {
|
|
||||||
public RequireCapabilityException(String msg) {
|
|
||||||
super(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -24,11 +24,10 @@ import com.google.gerrit.httpd.raw.LegacyGerritServlet;
|
|||||||
import com.google.gerrit.httpd.raw.SshInfoServlet;
|
import com.google.gerrit.httpd.raw.SshInfoServlet;
|
||||||
import com.google.gerrit.httpd.raw.StaticServlet;
|
import com.google.gerrit.httpd.raw.StaticServlet;
|
||||||
import com.google.gerrit.httpd.raw.ToolServlet;
|
import com.google.gerrit.httpd.raw.ToolServlet;
|
||||||
import com.google.gerrit.httpd.rpc.account.AccountCapabilitiesServlet;
|
import com.google.gerrit.httpd.rpc.account.AccountsRestApiServlet;
|
||||||
|
import com.google.gerrit.httpd.rpc.change.ChangesRestApiServlet;
|
||||||
import com.google.gerrit.httpd.rpc.change.DeprecatedChangeQueryServlet;
|
import com.google.gerrit.httpd.rpc.change.DeprecatedChangeQueryServlet;
|
||||||
import com.google.gerrit.httpd.rpc.change.ListChangesServlet;
|
import com.google.gerrit.httpd.rpc.project.ProjectsRestApiServlet;
|
||||||
import com.google.gerrit.httpd.rpc.dashboard.ListDashboardsServlet;
|
|
||||||
import com.google.gerrit.httpd.rpc.project.ListProjectsServlet;
|
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
import com.google.gerrit.reviewdb.client.Change;
|
||||||
import com.google.gerrit.reviewdb.client.Project;
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
import com.google.gerrit.server.config.GerritServerConfig;
|
import com.google.gerrit.server.config.GerritServerConfig;
|
||||||
@ -95,10 +94,9 @@ class UrlModule extends ServletModule {
|
|||||||
serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
|
serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
|
||||||
|
|
||||||
filter("/a/*").through(RequireIdentifiedUserFilter.class);
|
filter("/a/*").through(RequireIdentifiedUserFilter.class);
|
||||||
serveRegex("^/(?:a/)?accounts/self/capabilities$").with(AccountCapabilitiesServlet.class);
|
serveRegex("^/(?:a/)?accounts/(.*)$").with(AccountsRestApiServlet.class);
|
||||||
serveRegex("^/(?:a/)?changes/$").with(ListChangesServlet.class);
|
serveRegex("^/(?:a/)?changes/(.*)$").with(ChangesRestApiServlet.class);
|
||||||
serveRegex("^/(?:a/)?projects/(.*)?$").with(ListProjectsServlet.class);
|
serveRegex("^/(?:a/)?projects/(.*)?$").with(ProjectsRestApiServlet.class);
|
||||||
serveRegex("^/(?:a/)?dashboards/(.*)?$").with(ListDashboardsServlet.class);
|
|
||||||
|
|
||||||
if (cfg.deprecatedQuery) {
|
if (cfg.deprecatedQuery) {
|
||||||
serve("/query").with(DeprecatedChangeQueryServlet.class);
|
serve("/query").with(DeprecatedChangeQueryServlet.class);
|
||||||
|
@ -27,7 +27,6 @@ public class HttpPluginModule extends ServletModule {
|
|||||||
@Override
|
@Override
|
||||||
protected void configureServlets() {
|
protected void configureServlets() {
|
||||||
bind(HttpPluginServlet.class);
|
bind(HttpPluginServlet.class);
|
||||||
serve("/plugins/*").with(HttpPluginServlet.class);
|
|
||||||
serveRegex("^/(?:a/)?plugins/(.*)?$").with(HttpPluginServlet.class);
|
serveRegex("^/(?:a/)?plugins/(.*)?$").with(HttpPluginServlet.class);
|
||||||
|
|
||||||
bind(StartPluginListener.class)
|
bind(StartPluginListener.class)
|
||||||
|
@ -14,17 +14,19 @@
|
|||||||
|
|
||||||
package com.google.gerrit.httpd.plugins;
|
package com.google.gerrit.httpd.plugins;
|
||||||
|
|
||||||
|
import com.google.common.base.Splitter;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.cache.Cache;
|
import com.google.common.cache.Cache;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.google.gerrit.extensions.registration.RegistrationHandle;
|
import com.google.gerrit.extensions.registration.RegistrationHandle;
|
||||||
import com.google.gerrit.httpd.rpc.plugin.ListPluginsServlet;
|
import com.google.gerrit.httpd.restapi.RestApiServlet;
|
||||||
import com.google.gerrit.server.MimeUtilFileTypeRegistry;
|
import com.google.gerrit.server.MimeUtilFileTypeRegistry;
|
||||||
import com.google.gerrit.server.config.CanonicalWebUrl;
|
import com.google.gerrit.server.config.CanonicalWebUrl;
|
||||||
import com.google.gerrit.server.config.GerritServerConfig;
|
import com.google.gerrit.server.config.GerritServerConfig;
|
||||||
import com.google.gerrit.server.documentation.MarkdownFormatter;
|
import com.google.gerrit.server.documentation.MarkdownFormatter;
|
||||||
import com.google.gerrit.server.plugins.Plugin;
|
import com.google.gerrit.server.plugins.Plugin;
|
||||||
|
import com.google.gerrit.server.plugins.PluginsCollection;
|
||||||
import com.google.gerrit.server.plugins.ReloadPluginListener;
|
import com.google.gerrit.server.plugins.ReloadPluginListener;
|
||||||
import com.google.gerrit.server.plugins.StartPluginListener;
|
import com.google.gerrit.server.plugins.StartPluginListener;
|
||||||
import com.google.gerrit.server.ssh.SshInfo;
|
import com.google.gerrit.server.ssh.SshInfo;
|
||||||
@ -80,7 +82,7 @@ class HttpPluginServlet extends HttpServlet
|
|||||||
private final Cache<ResourceKey, Resource> resourceCache;
|
private final Cache<ResourceKey, Resource> resourceCache;
|
||||||
private final String sshHost;
|
private final String sshHost;
|
||||||
private final int sshPort;
|
private final int sshPort;
|
||||||
private final ListPluginsServlet listServlet;
|
private final RestApiServlet managerApi;
|
||||||
|
|
||||||
private List<Plugin> pending = Lists.newArrayList();
|
private List<Plugin> pending = Lists.newArrayList();
|
||||||
private String base;
|
private String base;
|
||||||
@ -92,11 +94,13 @@ class HttpPluginServlet extends HttpServlet
|
|||||||
@CanonicalWebUrl Provider<String> webUrl,
|
@CanonicalWebUrl Provider<String> webUrl,
|
||||||
@Named(HttpPluginModule.PLUGIN_RESOURCES) Cache<ResourceKey, Resource> cache,
|
@Named(HttpPluginModule.PLUGIN_RESOURCES) Cache<ResourceKey, Resource> cache,
|
||||||
@GerritServerConfig Config cfg,
|
@GerritServerConfig Config cfg,
|
||||||
SshInfo sshInfo, ListPluginsServlet listServlet) {
|
SshInfo sshInfo,
|
||||||
|
RestApiServlet.Globals globals,
|
||||||
|
PluginsCollection plugins) {
|
||||||
this.mimeUtil = mimeUtil;
|
this.mimeUtil = mimeUtil;
|
||||||
this.webUrl = webUrl;
|
this.webUrl = webUrl;
|
||||||
this.resourceCache = cache;
|
this.resourceCache = cache;
|
||||||
this.listServlet = listServlet;
|
this.managerApi = new RestApiServlet(globals, plugins);
|
||||||
|
|
||||||
String sshHost = "review.example.com";
|
String sshHost = "review.example.com";
|
||||||
int sshPort = 29418;
|
int sshPort = 29418;
|
||||||
@ -187,11 +191,16 @@ class HttpPluginServlet extends HttpServlet
|
|||||||
@Override
|
@Override
|
||||||
public void service(HttpServletRequest req, HttpServletResponse res)
|
public void service(HttpServletRequest req, HttpServletResponse res)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
String name = extractName(req);
|
List<String> parts = Lists.newArrayList(
|
||||||
if (name.equals("")) {
|
Splitter.on('/').limit(3).omitEmptyStrings()
|
||||||
listServlet.service(req, res);
|
.split(Strings.nullToEmpty(req.getPathInfo())));
|
||||||
|
|
||||||
|
if (isApiCall(req, parts)) {
|
||||||
|
managerApi.service(req, res);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String name = parts.get(0);
|
||||||
final PluginHolder holder = plugins.get(name);
|
final PluginHolder holder = plugins.get(name);
|
||||||
if (holder == null) {
|
if (holder == null) {
|
||||||
noCache(res);
|
noCache(res);
|
||||||
@ -214,6 +223,14 @@ class HttpPluginServlet extends HttpServlet
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isApiCall(HttpServletRequest req, List<String> parts) {
|
||||||
|
String method = req.getMethod();
|
||||||
|
int cnt = parts.size();
|
||||||
|
return cnt == 0
|
||||||
|
|| (cnt == 1 && ("PUT".equals(method) || "DELETE".equals(method)))
|
||||||
|
|| (cnt == 2 && parts.get(1).startsWith("gerrit~"));
|
||||||
|
}
|
||||||
|
|
||||||
private void onDefault(PluginHolder holder,
|
private void onDefault(PluginHolder holder,
|
||||||
HttpServletRequest req,
|
HttpServletRequest req,
|
||||||
HttpServletResponse res) throws IOException {
|
HttpServletResponse res) throws IOException {
|
||||||
@ -553,15 +570,6 @@ class HttpPluginServlet extends HttpServlet
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String extractName(HttpServletRequest req) {
|
|
||||||
String path = req.getPathInfo();
|
|
||||||
if (Strings.isNullOrEmpty(path) || "/".equals(path)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
int s = path.indexOf('/', 1);
|
|
||||||
return 0 <= s ? path.substring(1, s) : path.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void noCache(HttpServletResponse res) {
|
static void noCache(HttpServletResponse res) {
|
||||||
res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
|
res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
|
||||||
res.setHeader("Pragma", "no-cache");
|
res.setHeader("Pragma", "no-cache");
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.httpd.restapi;
|
||||||
|
|
||||||
|
import static com.google.gerrit.httpd.restapi.RestApiServlet.replyError;
|
||||||
|
import static com.google.gerrit.httpd.restapi.RestApiServlet.replyText;
|
||||||
|
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||||
|
|
||||||
|
import com.google.common.base.Splitter;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.common.collect.Multimap;
|
||||||
|
import com.google.gerrit.util.cli.CmdLineParser;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
import org.kohsuke.args4j.CmdLineException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
class ParameterParser {
|
||||||
|
private static final ImmutableSet<String> RESERVED_KEYS = ImmutableSet.of(
|
||||||
|
"pp", "prettyPrint", "strict", "callback", "alt", "fields");
|
||||||
|
|
||||||
|
private final CmdLineParser.Factory parserFactory;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ParameterParser(CmdLineParser.Factory pf) {
|
||||||
|
this.parserFactory = pf;
|
||||||
|
}
|
||||||
|
|
||||||
|
<T> boolean parse(T param,
|
||||||
|
Multimap<String, String> in,
|
||||||
|
HttpServletRequest req,
|
||||||
|
HttpServletResponse res)
|
||||||
|
throws IOException {
|
||||||
|
CmdLineParser clp = parserFactory.create(param);
|
||||||
|
try {
|
||||||
|
clp.parseOptionMap(in);
|
||||||
|
} catch (CmdLineException e) {
|
||||||
|
if (!clp.wasHelpRequestedByOption()) {
|
||||||
|
replyError(res, SC_BAD_REQUEST, e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clp.wasHelpRequestedByOption()) {
|
||||||
|
StringWriter msg = new StringWriter();
|
||||||
|
clp.printQueryStringUsage(req.getRequestURI(), msg);
|
||||||
|
msg.write('\n');
|
||||||
|
msg.write('\n');
|
||||||
|
clp.printUsage(msg, null);
|
||||||
|
msg.write('\n');
|
||||||
|
replyText(req, res, msg.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void splitQueryString(String queryString,
|
||||||
|
Multimap<String, String> config,
|
||||||
|
Multimap<String, String> params)
|
||||||
|
throws UnsupportedEncodingException {
|
||||||
|
if (!Strings.isNullOrEmpty(queryString)) {
|
||||||
|
for (String kvPair : Splitter.on('&').split(queryString)) {
|
||||||
|
Iterator<String> i = Splitter.on('=').limit(2).split(kvPair).iterator();
|
||||||
|
String key = decode(i.next());
|
||||||
|
String val = i.hasNext() ? decode(i.next()) : "";
|
||||||
|
if (RESERVED_KEYS.contains(key)) {
|
||||||
|
config.put(key, val);
|
||||||
|
} else {
|
||||||
|
params.put(key, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String decode(String value) throws UnsupportedEncodingException {
|
||||||
|
return URLDecoder.decode(value, "UTF-8");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,693 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.httpd.restapi;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||||
|
import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
|
||||||
|
import static javax.servlet.http.HttpServletResponse.SC_CREATED;
|
||||||
|
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||||
|
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||||
|
import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
|
||||||
|
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
|
||||||
|
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||||
|
|
||||||
|
import com.google.common.base.Function;
|
||||||
|
import com.google.common.base.Joiner;
|
||||||
|
import com.google.common.base.Splitter;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.google.common.collect.LinkedHashMultimap;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import com.google.common.collect.Multimap;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
import com.google.gerrit.extensions.annotations.RequiresCapability;
|
||||||
|
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||||
|
import com.google.gerrit.extensions.restapi.AcceptsCreate;
|
||||||
|
import com.google.gerrit.extensions.restapi.AuthException;
|
||||||
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
|
import com.google.gerrit.extensions.restapi.BinaryResult;
|
||||||
|
import com.google.gerrit.extensions.restapi.DefaultInput;
|
||||||
|
import com.google.gerrit.extensions.restapi.PutInput;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestCollection;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestResource;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestView;
|
||||||
|
import com.google.gerrit.extensions.restapi.TopLevelResource;
|
||||||
|
import com.google.gerrit.httpd.WebSession;
|
||||||
|
import com.google.gerrit.server.AccessPath;
|
||||||
|
import com.google.gerrit.server.AnonymousUser;
|
||||||
|
import com.google.gerrit.server.CurrentUser;
|
||||||
|
import com.google.gerrit.server.OutputFormat;
|
||||||
|
import com.google.gerrit.server.account.CapabilityControl;
|
||||||
|
import com.google.gson.ExclusionStrategy;
|
||||||
|
import com.google.gson.FieldAttributes;
|
||||||
|
import com.google.gson.FieldNamingPolicy;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
|
import com.google.gson.stream.JsonReader;
|
||||||
|
import com.google.gson.stream.JsonToken;
|
||||||
|
import com.google.gwtjsonrpc.common.JsonConstants;
|
||||||
|
import com.google.gwtjsonrpc.server.RPCServletUtils;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Provider;
|
||||||
|
import com.google.inject.util.Providers;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.util.TemporaryBuffer;
|
||||||
|
import org.eclipse.jgit.util.TemporaryBuffer.Heap;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
public class RestApiServlet extends HttpServlet {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private static final Logger log = LoggerFactory
|
||||||
|
.getLogger(RestApiServlet.class);
|
||||||
|
|
||||||
|
/** MIME type used for a JSON response body. */
|
||||||
|
private static final String JSON_TYPE = JsonConstants.JSON_TYPE;
|
||||||
|
private static final String UTF_8 = "UTF-8";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Garbage prefix inserted before JSON output to prevent XSSI.
|
||||||
|
* <p>
|
||||||
|
* This prefix is ")]}'\n" and is designed to prevent a web browser from
|
||||||
|
* executing the response body if the resource URI were to be referenced using
|
||||||
|
* a <script src="...> HTML tag from another web site. Clients using the
|
||||||
|
* HTTP interface will need to always strip the first line of response data to
|
||||||
|
* remove this magic header.
|
||||||
|
*/
|
||||||
|
private static final byte[] JSON_MAGIC;
|
||||||
|
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
JSON_MAGIC = ")]}'\n".getBytes(UTF_8);
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("UTF-8 not supported", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Globals {
|
||||||
|
final Provider<CurrentUser> currentUser;
|
||||||
|
final Provider<WebSession> webSession;
|
||||||
|
final Provider<ParameterParser> paramParser;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Globals(Provider<CurrentUser> currentUser,
|
||||||
|
Provider<WebSession> webSession,
|
||||||
|
Provider<ParameterParser> paramParser) {
|
||||||
|
this.currentUser = currentUser;
|
||||||
|
this.webSession = webSession;
|
||||||
|
this.paramParser = paramParser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Globals globals;
|
||||||
|
private final Provider<RestCollection<RestResource, RestResource>> members;
|
||||||
|
|
||||||
|
public RestApiServlet(Globals globals,
|
||||||
|
RestCollection<? extends RestResource, ? extends RestResource> members) {
|
||||||
|
this(globals, Providers.of(members));
|
||||||
|
}
|
||||||
|
|
||||||
|
public RestApiServlet(Globals globals,
|
||||||
|
Provider<? extends RestCollection<? extends RestResource, ? extends RestResource>> members) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Provider<RestCollection<RestResource, RestResource>> n =
|
||||||
|
(Provider<RestCollection<RestResource, RestResource>>) checkNotNull((Object) members);
|
||||||
|
this.globals = globals;
|
||||||
|
this.members = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final void service(HttpServletRequest req, HttpServletResponse res)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
|
||||||
|
res.setHeader("Pragma", "no-cache");
|
||||||
|
res.setHeader("Cache-Control", "no-cache, must-revalidate");
|
||||||
|
res.setHeader("Content-Disposition", "attachment");
|
||||||
|
|
||||||
|
try {
|
||||||
|
int status = SC_OK;
|
||||||
|
checkUserSession(req);
|
||||||
|
|
||||||
|
List<String> path = splitPath(req);
|
||||||
|
RestCollection<RestResource, RestResource> rc = members.get();
|
||||||
|
checkAccessAnnotations(rc.getClass());
|
||||||
|
|
||||||
|
RestResource rsrc = TopLevelResource.INSTANCE;
|
||||||
|
RestView<RestResource> view = null;
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
view = rc.list();
|
||||||
|
} else {
|
||||||
|
String id = path.remove(0);
|
||||||
|
try {
|
||||||
|
rsrc = rc.parse(rsrc, id);
|
||||||
|
} catch (ResourceNotFoundException e) {
|
||||||
|
if (rc instanceof AcceptsCreate
|
||||||
|
&& ("POST".equals(req.getMethod())
|
||||||
|
|| "PUT".equals(req.getMethod()))) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
AcceptsCreate<RestResource> ac = (AcceptsCreate<RestResource>) rc;
|
||||||
|
view = ac.create(rsrc, id);
|
||||||
|
status = SC_CREATED;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (view == null) {
|
||||||
|
view = view(rc, req.getMethod(), path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkAccessAnnotations(view.getClass());
|
||||||
|
|
||||||
|
while (view instanceof RestCollection<?,?>) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
RestCollection<RestResource, RestResource> c =
|
||||||
|
(RestCollection<RestResource, RestResource>) view;
|
||||||
|
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
view = c.list();
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
rsrc = c.parse(rsrc, path.remove(0));
|
||||||
|
view = view(c, req.getMethod(), path);
|
||||||
|
}
|
||||||
|
checkAccessAnnotations(view.getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
Multimap<String, String> config = LinkedHashMultimap.create();
|
||||||
|
Multimap<String, String> params = LinkedHashMultimap.create();
|
||||||
|
ParameterParser.splitQueryString(req.getQueryString(), config, params);
|
||||||
|
if (!globals.paramParser.get().parse(view, params, req, res)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object result;
|
||||||
|
if (view instanceof RestModifyView<?, ?>) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
RestModifyView<RestResource, Object> m =
|
||||||
|
(RestModifyView<RestResource, Object>) view;
|
||||||
|
|
||||||
|
result = m.apply(rsrc, parseRequest(req, m.inputType()));
|
||||||
|
} else if (view instanceof RestReadView<?>) {
|
||||||
|
result = ((RestReadView<RestResource>) view).apply(rsrc);
|
||||||
|
} else {
|
||||||
|
throw new ResourceNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setStatus(status);
|
||||||
|
if (result instanceof BinaryResult) {
|
||||||
|
replyBinaryResult(req, res, (BinaryResult) result);
|
||||||
|
} else {
|
||||||
|
replyJson(req, res, config, result);
|
||||||
|
}
|
||||||
|
} catch (AuthException e) {
|
||||||
|
replyError(res, SC_FORBIDDEN, e.getMessage());
|
||||||
|
} catch (BadRequestException e) {
|
||||||
|
replyError(res, SC_BAD_REQUEST, e.getMessage());
|
||||||
|
} catch (InvalidMethodException e) {
|
||||||
|
replyError(res, SC_METHOD_NOT_ALLOWED, "Method not allowed");
|
||||||
|
} catch (ResourceConflictException e) {
|
||||||
|
replyError(res, SC_CONFLICT, e.getMessage());
|
||||||
|
} catch (ResourceNotFoundException e) {
|
||||||
|
replyError(res, SC_NOT_FOUND, "Not found");
|
||||||
|
} catch (AmbiguousViewException e) {
|
||||||
|
replyError(res, SC_NOT_FOUND, e.getMessage());
|
||||||
|
} catch (JsonParseException e) {
|
||||||
|
replyError(res, SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleException(e, req, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object parseRequest(HttpServletRequest req, Class<Object> type)
|
||||||
|
throws IOException, BadRequestException, SecurityException,
|
||||||
|
IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
|
||||||
|
InstantiationException, InvocationTargetException, InvalidMethodException {
|
||||||
|
if (isType(JSON_TYPE, req.getContentType())) {
|
||||||
|
BufferedReader br = req.getReader();
|
||||||
|
try {
|
||||||
|
JsonReader json = new JsonReader(br);
|
||||||
|
JsonToken first;
|
||||||
|
try {
|
||||||
|
first = json.peek();
|
||||||
|
} catch (EOFException e) {
|
||||||
|
throw new BadRequestException("Expected JSON object");
|
||||||
|
}
|
||||||
|
if (first == JsonToken.STRING) {
|
||||||
|
return parseString(json.nextString(), type);
|
||||||
|
}
|
||||||
|
return OutputFormat.JSON.newGson().fromJson(json, type);
|
||||||
|
} finally {
|
||||||
|
br.close();
|
||||||
|
}
|
||||||
|
} else if ("PUT".equals(req.getMethod()) && acceptsPutInput(type)) {
|
||||||
|
return parsePutInput(req, type);
|
||||||
|
} else if ("DELETE".equals(req.getMethod()) && hasNoBody(req)) {
|
||||||
|
return null;
|
||||||
|
} else if (type.getDeclaredFields().length == 0 && hasNoBody(req)) {
|
||||||
|
return createInstance(type);
|
||||||
|
} else if (isType("text/plain", req.getContentType())) {
|
||||||
|
BufferedReader br = req.getReader();
|
||||||
|
try {
|
||||||
|
char[] tmp = new char[256];
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
int n;
|
||||||
|
while (0 < (n = br.read(tmp))) {
|
||||||
|
sb.append(tmp, 0, n);
|
||||||
|
}
|
||||||
|
return parseString(sb.toString(), type);
|
||||||
|
} finally {
|
||||||
|
br.close();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new BadRequestException("Expected Content-Type: " + JSON_TYPE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasNoBody(HttpServletRequest req) {
|
||||||
|
int len = req.getContentLength();
|
||||||
|
String type = req.getContentType();
|
||||||
|
return (len <= 0 && type == null)
|
||||||
|
|| (len == 0 && isType("application/x-www-form-urlencoded", type));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean acceptsPutInput(Class<Object> type) {
|
||||||
|
for (Field f : type.getDeclaredFields()) {
|
||||||
|
if (f.getType() == PutInput.class) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object parsePutInput(final HttpServletRequest req, Class<Object> type)
|
||||||
|
throws SecurityException, NoSuchMethodException,
|
||||||
|
IllegalArgumentException, InstantiationException, IllegalAccessException,
|
||||||
|
InvocationTargetException, InvalidMethodException {
|
||||||
|
Object obj = createInstance(type);
|
||||||
|
for (Field f : type.getDeclaredFields()) {
|
||||||
|
if (f.getType() == PutInput.class) {
|
||||||
|
f.setAccessible(true);
|
||||||
|
f.set(obj, new PutInput() {
|
||||||
|
@Override
|
||||||
|
public String getContentType() {
|
||||||
|
return req.getContentType();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getContentLength() {
|
||||||
|
return req.getContentLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() throws IOException {
|
||||||
|
return req.getInputStream();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new InvalidMethodException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object parseString(String value, Class<Object> type)
|
||||||
|
throws BadRequestException, SecurityException, NoSuchMethodException,
|
||||||
|
IllegalArgumentException, IllegalAccessException, InstantiationException,
|
||||||
|
InvocationTargetException {
|
||||||
|
Object obj = createInstance(type);
|
||||||
|
Field[] fields = type.getDeclaredFields();
|
||||||
|
if (fields.length == 0 && Strings.isNullOrEmpty(value)) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
for (Field f : fields) {
|
||||||
|
if (f.getAnnotation(DefaultInput.class) != null
|
||||||
|
&& f.getType() == String.class) {
|
||||||
|
f.setAccessible(true);
|
||||||
|
f.set(obj, value);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new BadRequestException("Expected JSON object");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object createInstance(Class<Object> type)
|
||||||
|
throws NoSuchMethodException, InstantiationException,
|
||||||
|
IllegalAccessException, InvocationTargetException {
|
||||||
|
Constructor<Object> c = type.getDeclaredConstructor();
|
||||||
|
c.setAccessible(true);
|
||||||
|
return c.newInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void replyJson(HttpServletRequest req,
|
||||||
|
HttpServletResponse res,
|
||||||
|
Multimap<String, String> config, Object result)
|
||||||
|
throws IOException {
|
||||||
|
final TemporaryBuffer.Heap buf = heap(Integer.MAX_VALUE);
|
||||||
|
buf.write(JSON_MAGIC);
|
||||||
|
Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
|
||||||
|
Gson gson = newGson(config, req);
|
||||||
|
if (result instanceof JsonElement) {
|
||||||
|
gson.toJson((JsonElement) result, w);
|
||||||
|
} else {
|
||||||
|
gson.toJson(result, w);
|
||||||
|
}
|
||||||
|
w.write('\n');
|
||||||
|
w.flush();
|
||||||
|
|
||||||
|
replyBinaryResult(req, res, new BinaryResult() {
|
||||||
|
@Override
|
||||||
|
public long getContentLength() {
|
||||||
|
return buf.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(OutputStream os) throws IOException {
|
||||||
|
buf.writeTo(os, null);
|
||||||
|
}
|
||||||
|
}.setContentType(JSON_TYPE).setCharacterEncoding(UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final FieldNamingPolicy NAMING =
|
||||||
|
FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES;
|
||||||
|
|
||||||
|
private static Gson newGson(Multimap<String, String> config,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
GsonBuilder gb = OutputFormat.JSON_COMPACT.newGsonBuilder()
|
||||||
|
.setFieldNamingPolicy(NAMING);
|
||||||
|
|
||||||
|
enablePrettyPrint(gb, config, req);
|
||||||
|
enablePartialGetFields(gb, config);
|
||||||
|
|
||||||
|
return gb.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void enablePrettyPrint(GsonBuilder gb,
|
||||||
|
Multimap<String, String> config, HttpServletRequest req) {
|
||||||
|
String pp = Iterables.getFirst(config.get("pp"), null);
|
||||||
|
if (pp == null) {
|
||||||
|
pp = Iterables.getFirst(config.get("prettyPrint"), null);
|
||||||
|
if (pp == null) {
|
||||||
|
pp = acceptsJson(req) ? "0" : "1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("1".equals(pp) || "true".equals(pp)) {
|
||||||
|
gb.setPrettyPrinting();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void enablePartialGetFields(GsonBuilder gb,
|
||||||
|
Multimap<String, String> config) {
|
||||||
|
final Set<String> want = Sets.newHashSet();
|
||||||
|
for (String p : config.get("fields")) {
|
||||||
|
Iterables.addAll(want, Splitter.on(',')
|
||||||
|
.omitEmptyStrings().trimResults()
|
||||||
|
.split(p));
|
||||||
|
}
|
||||||
|
if (!want.isEmpty()) {
|
||||||
|
gb.addSerializationExclusionStrategy(new ExclusionStrategy() {
|
||||||
|
private final Map<String, String> names = Maps.newHashMap();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldSkipField(FieldAttributes field) {
|
||||||
|
String name = names.get(field.getName());
|
||||||
|
if (name == null) {
|
||||||
|
// Names are supplied by Gson in terms of Java source.
|
||||||
|
// Translate and cache the JSON lower_case_style used.
|
||||||
|
try {
|
||||||
|
name = NAMING.translateName(
|
||||||
|
field.getDeclaringClass().getDeclaredField(field.getName()));
|
||||||
|
names.put(field.getName(), name);
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
return true;
|
||||||
|
} catch (NoSuchFieldException e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !want.contains(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldSkipClass(Class<?> clazz) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void replyBinaryResult(HttpServletRequest req,
|
||||||
|
HttpServletResponse res, BinaryResult bin) throws IOException {
|
||||||
|
try {
|
||||||
|
res.setContentType(bin.getContentType());
|
||||||
|
OutputStream dst = res.getOutputStream();
|
||||||
|
try {
|
||||||
|
long len = bin.getContentLength();
|
||||||
|
boolean gzip = bin.canGzip() && acceptsGzip(req);
|
||||||
|
if (gzip && 256 <= len && len <= (10 << 20)) {
|
||||||
|
TemporaryBuffer.Heap buf = compress(bin);
|
||||||
|
res.setContentLength((int) buf.length());
|
||||||
|
res.setHeader("Content-Encoding", "gzip");
|
||||||
|
buf.writeTo(dst, null);
|
||||||
|
} else if (gzip) {
|
||||||
|
res.setHeader("Content-Encoding", "gzip");
|
||||||
|
dst = new GZIPOutputStream(dst);
|
||||||
|
bin.writeTo(dst);
|
||||||
|
} else {
|
||||||
|
if (0 <= len && len < Integer.MAX_VALUE) {
|
||||||
|
res.setContentLength((int) len);
|
||||||
|
} else if (0 <= len) {
|
||||||
|
res.setHeader("Content-Length", Long.toString(len));
|
||||||
|
}
|
||||||
|
bin.writeTo(dst);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
dst.close();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
bin.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RestView<RestResource> view(
|
||||||
|
RestCollection<RestResource, RestResource> rc,
|
||||||
|
String method, List<String> path) throws ResourceNotFoundException,
|
||||||
|
InvalidMethodException, AmbiguousViewException {
|
||||||
|
DynamicMap<RestView<RestResource>> views = rc.views();
|
||||||
|
final String projection = path.isEmpty() ? "/" : path.remove(0);
|
||||||
|
if (!path.isEmpty()) {
|
||||||
|
// If there are path components still remaining after this projection
|
||||||
|
// is chosen, look for the projection based upon GET as the method as
|
||||||
|
// the client thinks it is a nested collection.
|
||||||
|
method = "GET";
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> p = splitProjection(projection);
|
||||||
|
if (p.size() == 2) {
|
||||||
|
RestView<RestResource> view =
|
||||||
|
views.get(p.get(0), method + "." + p.get(1));
|
||||||
|
if (view != null) {
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
throw new ResourceNotFoundException(projection);
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = method + "." + p.get(0);
|
||||||
|
RestView<RestResource> core = views.get("gerrit", name);
|
||||||
|
if (core != null) {
|
||||||
|
return core;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, RestView<RestResource>> r = Maps.newTreeMap();
|
||||||
|
for (String plugin : views.plugins()) {
|
||||||
|
RestView<RestResource> action = views.get(plugin, name);
|
||||||
|
if (action != null) {
|
||||||
|
r.put(plugin, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r.size() == 1) {
|
||||||
|
return Iterables.getFirst(r.values(), null);
|
||||||
|
} else if (r.isEmpty()) {
|
||||||
|
throw new ResourceNotFoundException(projection);
|
||||||
|
} else {
|
||||||
|
throw new AmbiguousViewException(String.format(
|
||||||
|
"Projection %s is ambiguous: ",
|
||||||
|
name,
|
||||||
|
Joiner.on(", ").join(
|
||||||
|
Iterables.transform(r.keySet(), new Function<String, String>() {
|
||||||
|
@Override
|
||||||
|
public String apply(String in) {
|
||||||
|
return in + "~" + projection;
|
||||||
|
}
|
||||||
|
}))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> splitPath(HttpServletRequest req) {
|
||||||
|
String path = req.getPathInfo();
|
||||||
|
if (Strings.isNullOrEmpty(path)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<String> out = Lists.newArrayList(Splitter.on('/').split(path));
|
||||||
|
if (out.size() > 0 && out.get(out.size() - 1).isEmpty()) {
|
||||||
|
out.remove(out.size() - 1);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> splitProjection(String projection) {
|
||||||
|
return Lists.newArrayList(Splitter.on('~').limit(2).split(projection));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkUserSession(HttpServletRequest req)
|
||||||
|
throws AuthException {
|
||||||
|
CurrentUser user = globals.currentUser.get();
|
||||||
|
if (isStateChange(req)) {
|
||||||
|
if (user instanceof AnonymousUser) {
|
||||||
|
throw new AuthException("Authentication required");
|
||||||
|
} else if (!globals.webSession.get().isAccessPathOk(AccessPath.REST_API)) {
|
||||||
|
throw new AuthException("Invalid authentication method");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user.setAccessPath(AccessPath.REST_API);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isStateChange(HttpServletRequest req) {
|
||||||
|
String method = req.getMethod();
|
||||||
|
return !("GET".equals(method) || "HEAD".equals(method));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkAccessAnnotations(Class<? extends Object> clazz)
|
||||||
|
throws AuthException {
|
||||||
|
RequiresCapability rc = clazz.getAnnotation(RequiresCapability.class);
|
||||||
|
if (rc != null) {
|
||||||
|
CurrentUser user = globals.currentUser.get();
|
||||||
|
CapabilityControl ctl = user.getCapabilities();
|
||||||
|
if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
|
||||||
|
throw new AuthException(String.format(
|
||||||
|
"Capability %s is required to access this resource",
|
||||||
|
rc.value()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleException(Throwable err, HttpServletRequest req,
|
||||||
|
HttpServletResponse res) throws IOException {
|
||||||
|
String uri = req.getRequestURI();
|
||||||
|
if (!Strings.isNullOrEmpty(req.getQueryString())) {
|
||||||
|
uri += "?" + req.getQueryString();
|
||||||
|
}
|
||||||
|
log.error(String.format("Error in %s %s", req.getMethod(), uri), err);
|
||||||
|
|
||||||
|
if (!res.isCommitted()) {
|
||||||
|
res.reset();
|
||||||
|
replyError(res, SC_INTERNAL_SERVER_ERROR, "Internal server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void replyError(HttpServletResponse res, int statusCode, String msg)
|
||||||
|
throws IOException {
|
||||||
|
res.setStatus(statusCode);
|
||||||
|
replyText(null, res, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void replyText(@Nullable HttpServletRequest req,
|
||||||
|
HttpServletResponse res, String text) throws IOException {
|
||||||
|
if (!text.endsWith("\n")) {
|
||||||
|
text += "\n";
|
||||||
|
}
|
||||||
|
replyBinaryResult(req, res,
|
||||||
|
BinaryResult.create(text).setContentType("text/plain"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean acceptsJson(HttpServletRequest req) {
|
||||||
|
return req != null && isType(JSON_TYPE, req.getHeader("Accept"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean acceptsGzip(HttpServletRequest req) {
|
||||||
|
return req != null && RPCServletUtils.acceptsGzipEncoding(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isType(String expect, String given) {
|
||||||
|
if (given == null) {
|
||||||
|
return false;
|
||||||
|
} else if (expect.equals(given)) {
|
||||||
|
return true;
|
||||||
|
} else if (given.startsWith(expect + ",")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (String p : given.split("[ ,;][ ,;]*")) {
|
||||||
|
if (expect.equals(p)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TemporaryBuffer.Heap compress(BinaryResult bin)
|
||||||
|
throws IOException {
|
||||||
|
TemporaryBuffer.Heap buf = heap(20 << 20);
|
||||||
|
GZIPOutputStream gz = new GZIPOutputStream(buf);
|
||||||
|
bin.writeTo(gz);
|
||||||
|
gz.finish();
|
||||||
|
gz.flush();
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Heap heap(int max) {
|
||||||
|
return new TemporaryBuffer.Heap(max);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
private static class InvalidMethodException extends Exception {
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
private static class AmbiguousViewException extends Exception {
|
||||||
|
AmbiguousViewException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,189 +0,0 @@
|
|||||||
// Copyright (C) 2012 The Android Open Source Project
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package com.google.gerrit.httpd.rpc.account;
|
|
||||||
|
|
||||||
import static com.google.gerrit.common.data.GlobalCapability.CREATE_ACCOUNT;
|
|
||||||
import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
|
|
||||||
import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
|
|
||||||
import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
|
|
||||||
import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
|
|
||||||
import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
|
|
||||||
import static com.google.gerrit.common.data.GlobalCapability.START_REPLICATION;
|
|
||||||
import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
|
|
||||||
import static com.google.gerrit.common.data.GlobalCapability.VIEW_CONNECTIONS;
|
|
||||||
import static com.google.gerrit.common.data.GlobalCapability.VIEW_QUEUE;
|
|
||||||
|
|
||||||
import com.google.common.collect.Maps;
|
|
||||||
import com.google.common.collect.Sets;
|
|
||||||
import com.google.gerrit.common.data.GlobalCapability;
|
|
||||||
import com.google.gerrit.common.data.PermissionRange;
|
|
||||||
import com.google.gerrit.httpd.RestApiServlet;
|
|
||||||
import com.google.gerrit.server.CurrentUser;
|
|
||||||
import com.google.gerrit.server.OutputFormat;
|
|
||||||
import com.google.gerrit.server.account.CapabilityControl;
|
|
||||||
import com.google.gerrit.server.git.QueueProvider;
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import com.google.inject.Provider;
|
|
||||||
import com.google.inject.Singleton;
|
|
||||||
|
|
||||||
import org.kohsuke.args4j.Option;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class AccountCapabilitiesServlet extends RestApiServlet {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
private final ParameterParser paramParser;
|
|
||||||
private final Provider<Impl> factory;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
AccountCapabilitiesServlet(final Provider<CurrentUser> currentUser,
|
|
||||||
ParameterParser paramParser, Provider<Impl> factory) {
|
|
||||||
super(currentUser);
|
|
||||||
this.paramParser = paramParser;
|
|
||||||
this.factory = factory;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doGet(HttpServletRequest req, HttpServletResponse res)
|
|
||||||
throws IOException {
|
|
||||||
Impl impl = factory.get();
|
|
||||||
if (acceptsJson(req)) {
|
|
||||||
impl.format = OutputFormat.JSON_COMPACT;
|
|
||||||
}
|
|
||||||
if (paramParser.parse(impl, req, res)) {
|
|
||||||
impl.compute();
|
|
||||||
|
|
||||||
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
|
||||||
OutputStreamWriter out = new OutputStreamWriter(buf, "UTF-8");
|
|
||||||
if (impl.format.isJson()) {
|
|
||||||
res.setContentType(JSON_TYPE);
|
|
||||||
buf.write(JSON_MAGIC);
|
|
||||||
impl.format.newGson().toJson(
|
|
||||||
impl.have,
|
|
||||||
new TypeToken<Map<String, Object>>() {}.getType(),
|
|
||||||
out);
|
|
||||||
out.flush();
|
|
||||||
buf.write('\n');
|
|
||||||
} else {
|
|
||||||
res.setContentType("text/plain");
|
|
||||||
for (Map.Entry<String, Object> e : impl.have.entrySet()) {
|
|
||||||
out.write(e.getKey());
|
|
||||||
if (!(e.getValue() instanceof Boolean)) {
|
|
||||||
out.write(": ");
|
|
||||||
out.write(e.getValue().toString());
|
|
||||||
}
|
|
||||||
out.write('\n');
|
|
||||||
}
|
|
||||||
out.flush();
|
|
||||||
}
|
|
||||||
res.setCharacterEncoding("UTF-8");
|
|
||||||
send(req, res, buf.toByteArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class Impl {
|
|
||||||
private final CapabilityControl cc;
|
|
||||||
private final Map<String, Object> have;
|
|
||||||
|
|
||||||
@Option(name = "--format", metaVar = "FMT", usage = "Output display format")
|
|
||||||
private OutputFormat format = OutputFormat.TEXT;
|
|
||||||
|
|
||||||
@Option(name = "-q", metaVar = "CAP", multiValued = true, usage = "Capability to inspect")
|
|
||||||
void addQuery(String name) {
|
|
||||||
if (query == null) {
|
|
||||||
query = Sets.newHashSet();
|
|
||||||
}
|
|
||||||
query.add(name.toLowerCase());
|
|
||||||
}
|
|
||||||
private Set<String> query;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
Impl(CurrentUser user) {
|
|
||||||
cc = user.getCapabilities();
|
|
||||||
have = Maps.newLinkedHashMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
void compute() {
|
|
||||||
for (String name : GlobalCapability.getAllNames()) {
|
|
||||||
if (!name.equals(PRIORITY) && want(name) && cc.canPerform(name)) {
|
|
||||||
if (GlobalCapability.hasRange(name)) {
|
|
||||||
have.put(name, new Range(cc.getRange(name)));
|
|
||||||
} else {
|
|
||||||
have.put(name, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
have.put(CREATE_ACCOUNT, cc.canCreateAccount());
|
|
||||||
have.put(CREATE_GROUP, cc.canCreateGroup());
|
|
||||||
have.put(CREATE_PROJECT, cc.canCreateProject());
|
|
||||||
have.put(KILL_TASK, cc.canKillTask());
|
|
||||||
have.put(VIEW_CACHES, cc.canViewCaches());
|
|
||||||
have.put(FLUSH_CACHES, cc.canFlushCaches());
|
|
||||||
have.put(VIEW_CONNECTIONS, cc.canViewConnections());
|
|
||||||
have.put(VIEW_QUEUE, cc.canViewQueue());
|
|
||||||
have.put(START_REPLICATION, cc.canStartReplication());
|
|
||||||
|
|
||||||
QueueProvider.QueueType queue = cc.getQueueType();
|
|
||||||
if (queue != QueueProvider.QueueType.INTERACTIVE
|
|
||||||
|| (query != null && query.contains(PRIORITY))) {
|
|
||||||
have.put(PRIORITY, queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterator<Map.Entry<String, Object>> itr = have.entrySet().iterator();
|
|
||||||
while (itr.hasNext()) {
|
|
||||||
Map.Entry<String, Object> e = itr.next();
|
|
||||||
if (!want(e.getKey())) {
|
|
||||||
itr.remove();
|
|
||||||
} else if (e.getValue() instanceof Boolean && !((Boolean) e.getValue())) {
|
|
||||||
itr.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean want(String name) {
|
|
||||||
return query == null || query.contains(name.toLowerCase());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Range {
|
|
||||||
private transient PermissionRange range;
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private int min;
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private int max;
|
|
||||||
|
|
||||||
Range(PermissionRange r) {
|
|
||||||
range = r;
|
|
||||||
min = r.getMin();
|
|
||||||
max = r.getMax();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return range.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.httpd.rpc.account;
|
||||||
|
|
||||||
|
import com.google.gerrit.httpd.restapi.RestApiServlet;
|
||||||
|
import com.google.gerrit.server.account.AccountsCollection;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Provider;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class AccountsRestApiServlet extends RestApiServlet {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AccountsRestApiServlet(RestApiServlet.Globals globals,
|
||||||
|
Provider<AccountsCollection> accounts) {
|
||||||
|
super(globals, accounts);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.httpd.rpc.change;
|
||||||
|
|
||||||
|
import com.google.gerrit.httpd.restapi.RestApiServlet;
|
||||||
|
import com.google.gerrit.server.change.ChangesCollection;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Provider;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class ChangesRestApiServlet extends RestApiServlet {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ChangesRestApiServlet(RestApiServlet.Globals globals,
|
||||||
|
Provider<ChangesCollection> changes) {
|
||||||
|
super(globals, changes);
|
||||||
|
}
|
||||||
|
}
|
@ -1,86 +0,0 @@
|
|||||||
// Copyright (C) 2012 The Android Open Source Project
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package com.google.gerrit.httpd.rpc.change;
|
|
||||||
|
|
||||||
import com.google.gerrit.httpd.RestApiServlet;
|
|
||||||
import com.google.gerrit.server.CurrentUser;
|
|
||||||
import com.google.gerrit.server.OutputFormat;
|
|
||||||
import com.google.gerrit.server.query.QueryParseException;
|
|
||||||
import com.google.gerrit.server.query.change.ListChanges;
|
|
||||||
import com.google.gwtorm.server.OrmException;
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import com.google.inject.Provider;
|
|
||||||
import com.google.inject.Singleton;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.BufferedWriter;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.io.Writer;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class ListChangesServlet extends RestApiServlet {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ListChangesServlet.class);
|
|
||||||
private final ParameterParser paramParser;
|
|
||||||
private final Provider<ListChanges> factory;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
ListChangesServlet(final Provider<CurrentUser> currentUser,
|
|
||||||
ParameterParser paramParser, Provider<ListChanges> ls) {
|
|
||||||
super(currentUser);
|
|
||||||
this.paramParser = paramParser;
|
|
||||||
this.factory = ls;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doGet(HttpServletRequest req, HttpServletResponse res)
|
|
||||||
throws IOException {
|
|
||||||
ListChanges impl = factory.get();
|
|
||||||
if (acceptsJson(req)) {
|
|
||||||
impl.setFormat(OutputFormat.JSON_COMPACT);
|
|
||||||
}
|
|
||||||
if (paramParser.parse(impl, req, res)) {
|
|
||||||
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
|
||||||
if (impl.getFormat().isJson()) {
|
|
||||||
buf.write(JSON_MAGIC);
|
|
||||||
}
|
|
||||||
|
|
||||||
Writer out = new BufferedWriter(new OutputStreamWriter(buf, "UTF-8"));
|
|
||||||
try {
|
|
||||||
impl.query(out);
|
|
||||||
} catch (QueryParseException e) {
|
|
||||||
res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
|
||||||
sendText(req, res, e.getMessage());
|
|
||||||
return;
|
|
||||||
} catch (OrmException e) {
|
|
||||||
log.error("Error querying /changes/", e);
|
|
||||||
res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
out.flush();
|
|
||||||
|
|
||||||
res.setContentType(impl.getFormat().isJson() ? JSON_TYPE : "text/plain");
|
|
||||||
res.setCharacterEncoding("UTF-8");
|
|
||||||
send(req, res, buf.toByteArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
// Copyright (C) 2012 The Android Open Source Project
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package com.google.gerrit.httpd.rpc.dashboard;
|
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
|
||||||
import com.google.gerrit.httpd.RestApiServlet;
|
|
||||||
import com.google.gerrit.server.CurrentUser;
|
|
||||||
import com.google.gerrit.server.OutputFormat;
|
|
||||||
import com.google.gerrit.server.dashboard.ListDashboards;
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import com.google.inject.Provider;
|
|
||||||
import com.google.inject.Singleton;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URLDecoder;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class ListDashboardsServlet extends RestApiServlet {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
private static final String PROJECT_LEVEL_PREFIX = "project/";
|
|
||||||
private final ParameterParser paramParser;
|
|
||||||
private final Provider<ListDashboards> factory;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
ListDashboardsServlet(final Provider<CurrentUser> currentUser,
|
|
||||||
ParameterParser paramParser, Provider<ListDashboards> ls) {
|
|
||||||
super(currentUser);
|
|
||||||
this.paramParser = paramParser;
|
|
||||||
this.factory = ls;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doGet(HttpServletRequest req, HttpServletResponse res)
|
|
||||||
throws IOException {
|
|
||||||
ListDashboards impl = factory.get();
|
|
||||||
if (!Strings.isNullOrEmpty(req.getPathInfo())) {
|
|
||||||
final String path = URLDecoder.decode(req.getPathInfo(), "UTF-8");
|
|
||||||
if (path.startsWith(PROJECT_LEVEL_PREFIX)) {
|
|
||||||
impl.setLevel(ListDashboards.Level.PROJECT);
|
|
||||||
impl.setEntityName(path.substring(PROJECT_LEVEL_PREFIX.length()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (acceptsJson(req)) {
|
|
||||||
impl.setFormat(OutputFormat.JSON_COMPACT);
|
|
||||||
}
|
|
||||||
if (paramParser.parse(impl, req, res)) {
|
|
||||||
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
|
||||||
if (impl.getFormat().isJson()) {
|
|
||||||
res.setContentType(JSON_TYPE);
|
|
||||||
buf.write(JSON_MAGIC);
|
|
||||||
} else {
|
|
||||||
res.setContentType("text/plain");
|
|
||||||
}
|
|
||||||
impl.display(buf);
|
|
||||||
res.setCharacterEncoding("UTF-8");
|
|
||||||
send(req, res, buf.toByteArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
// Copyright (C) 2012 The Android Open Source Project
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package com.google.gerrit.httpd.rpc.plugin;
|
|
||||||
|
|
||||||
import com.google.gerrit.common.data.GlobalCapability;
|
|
||||||
import com.google.gerrit.extensions.annotations.RequiresCapability;
|
|
||||||
import com.google.gerrit.httpd.RestApiServlet;
|
|
||||||
import com.google.gerrit.server.CurrentUser;
|
|
||||||
import com.google.gerrit.server.OutputFormat;
|
|
||||||
import com.google.gerrit.server.plugins.ListPlugins;
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import com.google.inject.Provider;
|
|
||||||
import com.google.inject.Singleton;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
|
|
||||||
public class ListPluginsServlet extends RestApiServlet {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
private final ParameterParser paramParser;
|
|
||||||
private final Provider<ListPlugins> factory;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
ListPluginsServlet(final Provider<CurrentUser> currentUser,
|
|
||||||
ParameterParser paramParser, Provider<ListPlugins> ls) {
|
|
||||||
super(currentUser);
|
|
||||||
this.paramParser = paramParser;
|
|
||||||
this.factory = ls;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doGet(HttpServletRequest req, HttpServletResponse res)
|
|
||||||
throws IOException {
|
|
||||||
ListPlugins impl = factory.get();
|
|
||||||
if (acceptsJson(req)) {
|
|
||||||
impl.setFormat(OutputFormat.JSON_COMPACT);
|
|
||||||
}
|
|
||||||
if (paramParser.parse(impl, req, res)) {
|
|
||||||
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
|
||||||
if (impl.getFormat().isJson()) {
|
|
||||||
res.setContentType(JSON_TYPE);
|
|
||||||
buf.write(JSON_MAGIC);
|
|
||||||
} else {
|
|
||||||
res.setContentType("text/plain");
|
|
||||||
}
|
|
||||||
impl.display(buf);
|
|
||||||
res.setCharacterEncoding("UTF-8");
|
|
||||||
send(req, res, buf.toByteArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
// Copyright (C) 2012 The Android Open Source Project
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package com.google.gerrit.httpd.rpc.project;
|
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
|
||||||
import com.google.gerrit.httpd.RestApiServlet;
|
|
||||||
import com.google.gerrit.server.CurrentUser;
|
|
||||||
import com.google.gerrit.server.OutputFormat;
|
|
||||||
import com.google.gerrit.server.project.ListProjects;
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import com.google.inject.Provider;
|
|
||||||
import com.google.inject.Singleton;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URLDecoder;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class ListProjectsServlet extends RestApiServlet {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
private final ParameterParser paramParser;
|
|
||||||
private final Provider<ListProjects> factory;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
ListProjectsServlet(final Provider<CurrentUser> currentUser,
|
|
||||||
ParameterParser paramParser, Provider<ListProjects> ls) {
|
|
||||||
super(currentUser);
|
|
||||||
this.paramParser = paramParser;
|
|
||||||
this.factory = ls;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doGet(HttpServletRequest req, HttpServletResponse res)
|
|
||||||
throws IOException {
|
|
||||||
ListProjects impl = factory.get();
|
|
||||||
if (!Strings.isNullOrEmpty(req.getPathInfo())) {
|
|
||||||
impl.setMatchPrefix(URLDecoder.decode(req.getPathInfo(), "UTF-8"));
|
|
||||||
}
|
|
||||||
if (acceptsJson(req)) {
|
|
||||||
impl.setFormat(OutputFormat.JSON_COMPACT);
|
|
||||||
}
|
|
||||||
if (paramParser.parse(impl, req, res)) {
|
|
||||||
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
|
||||||
if (impl.getFormat().isJson()) {
|
|
||||||
res.setContentType(JSON_TYPE);
|
|
||||||
buf.write(JSON_MAGIC);
|
|
||||||
} else {
|
|
||||||
res.setContentType("text/plain");
|
|
||||||
}
|
|
||||||
impl.display(buf);
|
|
||||||
res.setCharacterEncoding("UTF-8");
|
|
||||||
send(req, res, buf.toByteArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.httpd.rpc.project;
|
||||||
|
|
||||||
|
import com.google.gerrit.httpd.restapi.RestApiServlet;
|
||||||
|
import com.google.gerrit.server.project.ProjectsCollection;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Provider;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class ProjectsRestApiServlet extends RestApiServlet {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ProjectsRestApiServlet(RestApiServlet.Globals globals,
|
||||||
|
Provider<ProjectsCollection> projects) {
|
||||||
|
super(globals, projects);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.account;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.restapi.RestResource;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestView;
|
||||||
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
|
import com.google.inject.TypeLiteral;
|
||||||
|
|
||||||
|
public class AccountResource implements RestResource {
|
||||||
|
public static final TypeLiteral<RestView<AccountResource>> ACCOUNT_KIND =
|
||||||
|
new TypeLiteral<RestView<AccountResource>>() {};
|
||||||
|
|
||||||
|
private final IdentifiedUser user;
|
||||||
|
|
||||||
|
AccountResource(IdentifiedUser user) {
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IdentifiedUser getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.account;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||||
|
import com.google.gerrit.extensions.restapi.TopLevelResource;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestCollection;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestView;
|
||||||
|
import com.google.gerrit.server.CurrentUser;
|
||||||
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Provider;
|
||||||
|
|
||||||
|
public class AccountsCollection implements
|
||||||
|
RestCollection<TopLevelResource, AccountResource> {
|
||||||
|
private final Provider<CurrentUser> self;
|
||||||
|
private final DynamicMap<RestView<AccountResource>> views;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AccountsCollection(Provider<CurrentUser> self,
|
||||||
|
DynamicMap<RestView<AccountResource>> views) {
|
||||||
|
this.self = self;
|
||||||
|
this.views = views;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AccountResource parse(TopLevelResource root, String id)
|
||||||
|
throws ResourceNotFoundException {
|
||||||
|
if ("self".equals(id)) {
|
||||||
|
CurrentUser user = self.get();
|
||||||
|
if (user instanceof IdentifiedUser) {
|
||||||
|
return new AccountResource((IdentifiedUser) user);
|
||||||
|
}
|
||||||
|
throw new ResourceNotFoundException(id);
|
||||||
|
}
|
||||||
|
throw new ResourceNotFoundException(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RestView<TopLevelResource> list() throws ResourceNotFoundException {
|
||||||
|
throw new ResourceNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DynamicMap<RestView<AccountResource>> views() {
|
||||||
|
return views;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,140 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.account;
|
||||||
|
|
||||||
|
import static com.google.gerrit.common.data.GlobalCapability.CREATE_ACCOUNT;
|
||||||
|
import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
|
||||||
|
import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
|
||||||
|
import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
|
||||||
|
import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
|
||||||
|
import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
|
||||||
|
import static com.google.gerrit.common.data.GlobalCapability.START_REPLICATION;
|
||||||
|
import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
|
||||||
|
import static com.google.gerrit.common.data.GlobalCapability.VIEW_CONNECTIONS;
|
||||||
|
import static com.google.gerrit.common.data.GlobalCapability.VIEW_QUEUE;
|
||||||
|
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
import com.google.gerrit.common.data.GlobalCapability;
|
||||||
|
import com.google.gerrit.common.data.PermissionRange;
|
||||||
|
import com.google.gerrit.extensions.restapi.BinaryResult;
|
||||||
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||||
|
import com.google.gerrit.server.OutputFormat;
|
||||||
|
import com.google.gerrit.server.git.QueueProvider;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.kohsuke.args4j.Option;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class Capabilities implements RestReadView<AccountResource> {
|
||||||
|
@Deprecated
|
||||||
|
@Option(name = "--format", usage = "(deprecated) output format")
|
||||||
|
private OutputFormat format;
|
||||||
|
|
||||||
|
@Option(name = "-q", metaVar = "CAP", multiValued = true, usage = "Capability to inspect")
|
||||||
|
void addQuery(String name) {
|
||||||
|
if (query == null) {
|
||||||
|
query = Sets.newHashSet();
|
||||||
|
}
|
||||||
|
query.add(name.toLowerCase());
|
||||||
|
}
|
||||||
|
private Set<String> query;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object apply(AccountResource resource)
|
||||||
|
throws BadRequestException, Exception {
|
||||||
|
CapabilityControl cc = resource.getUser().getCapabilities();
|
||||||
|
Map<String, Object> have = Maps.newLinkedHashMap();
|
||||||
|
for (String name : GlobalCapability.getAllNames()) {
|
||||||
|
if (!name.equals(PRIORITY) && want(name) && cc.canPerform(name)) {
|
||||||
|
if (GlobalCapability.hasRange(name)) {
|
||||||
|
have.put(name, new Range(cc.getRange(name)));
|
||||||
|
} else {
|
||||||
|
have.put(name, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
have.put(CREATE_ACCOUNT, cc.canCreateAccount());
|
||||||
|
have.put(CREATE_GROUP, cc.canCreateGroup());
|
||||||
|
have.put(CREATE_PROJECT, cc.canCreateProject());
|
||||||
|
have.put(KILL_TASK, cc.canKillTask());
|
||||||
|
have.put(VIEW_CACHES, cc.canViewCaches());
|
||||||
|
have.put(FLUSH_CACHES, cc.canFlushCaches());
|
||||||
|
have.put(VIEW_CONNECTIONS, cc.canViewConnections());
|
||||||
|
have.put(VIEW_QUEUE, cc.canViewQueue());
|
||||||
|
have.put(START_REPLICATION, cc.canStartReplication());
|
||||||
|
|
||||||
|
QueueProvider.QueueType queue = cc.getQueueType();
|
||||||
|
if (queue != QueueProvider.QueueType.INTERACTIVE
|
||||||
|
|| (query != null && query.contains(PRIORITY))) {
|
||||||
|
have.put(PRIORITY, queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterator<Map.Entry<String, Object>> itr = have.entrySet().iterator();
|
||||||
|
while (itr.hasNext()) {
|
||||||
|
Map.Entry<String, Object> e = itr.next();
|
||||||
|
if (!want(e.getKey())) {
|
||||||
|
itr.remove();
|
||||||
|
} else if (e.getValue() instanceof Boolean && !((Boolean) e.getValue())) {
|
||||||
|
itr.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format == OutputFormat.TEXT) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (Map.Entry<String, Object> e : have.entrySet()) {
|
||||||
|
sb.append(e.getKey());
|
||||||
|
if (!(e.getValue() instanceof Boolean)) {
|
||||||
|
sb.append(": ");
|
||||||
|
sb.append(e.getValue().toString());
|
||||||
|
}
|
||||||
|
sb.append('\n');
|
||||||
|
}
|
||||||
|
return BinaryResult.create(sb.toString());
|
||||||
|
} else {
|
||||||
|
return OutputFormat.JSON.newGson().toJsonTree(
|
||||||
|
have,
|
||||||
|
new TypeToken<Map<String, Object>>() {}.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean want(String name) {
|
||||||
|
return query == null || query.contains(name.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Range {
|
||||||
|
private transient PermissionRange range;
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private int min;
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private int max;
|
||||||
|
|
||||||
|
Range(PermissionRange r) {
|
||||||
|
range = r;
|
||||||
|
min = r.getMin();
|
||||||
|
max = r.getMax();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return range.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.account;
|
||||||
|
|
||||||
|
import static com.google.gerrit.server.account.AccountResource.ACCOUNT_KIND;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestApiModule;
|
||||||
|
|
||||||
|
public class Module extends RestApiModule {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
DynamicMap.mapOf(binder(), ACCOUNT_KIND);
|
||||||
|
|
||||||
|
get(ACCOUNT_KIND, "capabilities").to(Capabilities.class);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.change;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.gerrit.common.ChangeHooks;
|
||||||
|
import com.google.gerrit.extensions.restapi.AuthException;
|
||||||
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||||
|
import com.google.gerrit.reviewdb.client.Change;
|
||||||
|
import com.google.gerrit.reviewdb.client.ChangeMessage;
|
||||||
|
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||||
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
|
import com.google.gerrit.server.ChangeUtil;
|
||||||
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
|
import com.google.gerrit.server.change.Abandon.Input;
|
||||||
|
import com.google.gerrit.server.mail.AbandonedSender;
|
||||||
|
import com.google.gerrit.server.project.ChangeControl;
|
||||||
|
import com.google.gwtorm.server.AtomicUpdate;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Provider;
|
||||||
|
|
||||||
|
public class Abandon implements RestModifyView<ChangeResource, Input> {
|
||||||
|
private final ChangeHooks hooks;
|
||||||
|
private final AbandonedSender.Factory abandonedSenderFactory;
|
||||||
|
private final Provider<ReviewDb> dbProvider;
|
||||||
|
private final ChangeJson json;
|
||||||
|
|
||||||
|
public static class Input {
|
||||||
|
String message;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Abandon(ChangeHooks hooks,
|
||||||
|
AbandonedSender.Factory abandonedSenderFactory,
|
||||||
|
Provider<ReviewDb> dbProvider,
|
||||||
|
ChangeJson json) {
|
||||||
|
this.hooks = hooks;
|
||||||
|
this.abandonedSenderFactory = abandonedSenderFactory;
|
||||||
|
this.dbProvider = dbProvider;
|
||||||
|
this.json = json;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<Input> inputType() {
|
||||||
|
return Input.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object apply(ChangeResource req, Input input)
|
||||||
|
throws BadRequestException, AuthException,
|
||||||
|
ResourceConflictException, Exception {
|
||||||
|
ChangeControl control = req.getControl();
|
||||||
|
Change change = req.getChange();
|
||||||
|
if (!control.canAbandon()) {
|
||||||
|
throw new AuthException("abandon not permitted");
|
||||||
|
} else if (!change.getStatus().isOpen()) {
|
||||||
|
throw new ResourceConflictException("change is " + status(change));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a message to accompany the abandoned change
|
||||||
|
ReviewDb db = dbProvider.get();
|
||||||
|
PatchSet.Id patchSetId = change.currentPatchSetId();
|
||||||
|
IdentifiedUser currentUser = (IdentifiedUser) control.getCurrentUser();
|
||||||
|
String message = Strings.emptyToNull(input.message);
|
||||||
|
ChangeMessage cmsg = new ChangeMessage(
|
||||||
|
new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db)),
|
||||||
|
currentUser.getAccountId(), patchSetId);
|
||||||
|
StringBuilder msg = new StringBuilder();
|
||||||
|
msg.append(String.format("Patch Set %d: Abandoned", patchSetId.get()));
|
||||||
|
if (message != null) {
|
||||||
|
msg.append("\n\n");
|
||||||
|
msg.append(message);
|
||||||
|
}
|
||||||
|
cmsg.setMessage(msg.toString());
|
||||||
|
|
||||||
|
// Abandon the change
|
||||||
|
Change updatedChange = db.changes().atomicUpdate(
|
||||||
|
change.getId(),
|
||||||
|
new AtomicUpdate<Change>() {
|
||||||
|
@Override
|
||||||
|
public Change update(Change change) {
|
||||||
|
if (change.getStatus().isOpen()) {
|
||||||
|
change.setStatus(Change.Status.ABANDONED);
|
||||||
|
ChangeUtil.updated(change);
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (updatedChange == null) {
|
||||||
|
throw new ResourceConflictException("change is "
|
||||||
|
+ status(db.changes().get(change.getId())));
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangeUtil.updatedChange(db, currentUser, updatedChange, cmsg,
|
||||||
|
abandonedSenderFactory);
|
||||||
|
hooks.doChangeAbandonedHook(updatedChange, currentUser.getAccount(),
|
||||||
|
message, db);
|
||||||
|
return json.format(change.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String status(Change change) {
|
||||||
|
return change != null ? change.getStatus().name().toLowerCase() : "deleted";
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package com.google.gerrit.server.query.change;
|
package com.google.gerrit.server.change;
|
||||||
|
|
||||||
import static com.google.gerrit.common.changes.ListChangesOption.ALL_COMMITS;
|
import static com.google.gerrit.common.changes.ListChangesOption.ALL_COMMITS;
|
||||||
import static com.google.gerrit.common.changes.ListChangesOption.ALL_FILES;
|
import static com.google.gerrit.common.changes.ListChangesOption.ALL_FILES;
|
||||||
@ -24,6 +24,7 @@ import static com.google.gerrit.common.changes.ListChangesOption.LABELS;
|
|||||||
|
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.google.gerrit.common.changes.ListChangesOption;
|
import com.google.gerrit.common.changes.ListChangesOption;
|
||||||
@ -43,7 +44,6 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
|
|||||||
import com.google.gerrit.server.AnonymousUser;
|
import com.google.gerrit.server.AnonymousUser;
|
||||||
import com.google.gerrit.server.CurrentUser;
|
import com.google.gerrit.server.CurrentUser;
|
||||||
import com.google.gerrit.server.IdentifiedUser;
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
import com.google.gerrit.server.OutputFormat;
|
|
||||||
import com.google.gerrit.server.config.CanonicalWebUrl;
|
import com.google.gerrit.server.config.CanonicalWebUrl;
|
||||||
import com.google.gerrit.server.config.GerritServerConfig;
|
import com.google.gerrit.server.config.GerritServerConfig;
|
||||||
import com.google.gerrit.server.events.AccountAttribute;
|
import com.google.gerrit.server.events.AccountAttribute;
|
||||||
@ -55,9 +55,8 @@ import com.google.gerrit.server.patch.PatchSetInfoFactory;
|
|||||||
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
|
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
|
||||||
import com.google.gerrit.server.project.ChangeControl;
|
import com.google.gerrit.server.project.ChangeControl;
|
||||||
import com.google.gerrit.server.project.NoSuchChangeException;
|
import com.google.gerrit.server.project.NoSuchChangeException;
|
||||||
import com.google.gerrit.server.query.QueryParseException;
|
import com.google.gerrit.server.query.change.ChangeData;
|
||||||
import com.google.gerrit.server.ssh.SshInfo;
|
import com.google.gerrit.server.ssh.SshInfo;
|
||||||
import com.google.gson.reflect.TypeToken;
|
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
@ -66,13 +65,10 @@ import com.google.inject.Singleton;
|
|||||||
import com.jcraft.jsch.HostKey;
|
import com.jcraft.jsch.HostKey;
|
||||||
|
|
||||||
import org.eclipse.jgit.lib.Config;
|
import org.eclipse.jgit.lib.Config;
|
||||||
import org.kohsuke.args4j.Option;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.io.Writer;
|
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.sql.Timestamp;
|
import java.sql.Timestamp;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@ -82,8 +78,8 @@ import java.util.EnumSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class ListChanges {
|
public class ChangeJson {
|
||||||
private static final Logger log = LoggerFactory.getLogger(ListChanges.class);
|
private static final Logger log = LoggerFactory.getLogger(ChangeJson.class);
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
static class Urls {
|
static class Urls {
|
||||||
@ -104,81 +100,39 @@ public class ListChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final QueryProcessor imp;
|
|
||||||
private final Provider<ReviewDb> db;
|
private final Provider<ReviewDb> db;
|
||||||
private final ApprovalTypes approvalTypes;
|
private final ApprovalTypes approvalTypes;
|
||||||
private final CurrentUser user;
|
private final CurrentUser user;
|
||||||
private final AnonymousUser anonymous;
|
private final AnonymousUser anonymous;
|
||||||
private final ChangeControl.Factory changeControlFactory;
|
private final ChangeControl.GenericFactory changeControlGenericFactory;
|
||||||
private final PatchSetInfoFactory patchSetInfoFactory;
|
private final PatchSetInfoFactory patchSetInfoFactory;
|
||||||
private final PatchListCache patchListCache;
|
private final PatchListCache patchListCache;
|
||||||
private final SshInfo sshInfo;
|
|
||||||
private final Provider<String> urlProvider;
|
private final Provider<String> urlProvider;
|
||||||
private final Urls urls;
|
private final Urls urls;
|
||||||
private boolean reverse;
|
private ChangeControl.Factory changeControlUserFactory;
|
||||||
|
private SshInfo sshInfo;
|
||||||
private Map<Account.Id, AccountAttribute> accounts;
|
private Map<Account.Id, AccountAttribute> accounts;
|
||||||
private Map<Change.Id, ChangeControl> controls;
|
private Map<Change.Id, ChangeControl> controls;
|
||||||
private EnumSet<ListChangesOption> options;
|
private EnumSet<ListChangesOption> options;
|
||||||
|
|
||||||
@Option(name = "--format", metaVar = "FMT", usage = "Output display format")
|
|
||||||
private OutputFormat format = OutputFormat.TEXT;
|
|
||||||
|
|
||||||
@Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", multiValued = true, usage = "Query string")
|
|
||||||
private List<String> queries;
|
|
||||||
|
|
||||||
@Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "Maximum number of results to return")
|
|
||||||
public void setLimit(int limit) {
|
|
||||||
imp.setLimit(limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Option(name = "-o", multiValued = true, usage = "Output options per change")
|
|
||||||
public void addOption(ListChangesOption o) {
|
|
||||||
options.add(o);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Option(name = "-O", usage = "Output option flags, in hex")
|
|
||||||
void setOptionFlagsHex(String hex) {
|
|
||||||
options.addAll(ListChangesOption.fromBits(Integer.parseInt(hex, 16)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Option(name = "-P", metaVar = "SORTKEY", usage = "Previous changes before SORTKEY")
|
|
||||||
public void setSortKeyAfter(String key) {
|
|
||||||
// Querying for the prior page of changes requires sortkey_after predicate.
|
|
||||||
// Changes are shown most recent->least recent. The previous page of
|
|
||||||
// results contains changes that were updated after the given key.
|
|
||||||
imp.setSortkeyAfter(key);
|
|
||||||
reverse = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Option(name = "-N", metaVar = "SORTKEY", usage = "Next changes after SORTKEY")
|
|
||||||
public void setSortKeyBefore(String key) {
|
|
||||||
// Querying for the next page of changes requires sortkey_before predicate.
|
|
||||||
// Changes are shown most recent->least recent. The next page contains
|
|
||||||
// changes that were updated before the given key.
|
|
||||||
imp.setSortkeyBefore(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ListChanges(QueryProcessor qp,
|
ChangeJson(
|
||||||
Provider<ReviewDb> db,
|
Provider<ReviewDb> db,
|
||||||
ApprovalTypes at,
|
ApprovalTypes at,
|
||||||
CurrentUser u,
|
CurrentUser u,
|
||||||
AnonymousUser au,
|
AnonymousUser au,
|
||||||
ChangeControl.Factory cf,
|
ChangeControl.GenericFactory gf,
|
||||||
PatchSetInfoFactory psi,
|
PatchSetInfoFactory psi,
|
||||||
PatchListCache plc,
|
PatchListCache plc,
|
||||||
SshInfo sshInfo,
|
|
||||||
@CanonicalWebUrl Provider<String> curl,
|
@CanonicalWebUrl Provider<String> curl,
|
||||||
Urls urls) {
|
Urls urls) {
|
||||||
this.imp = qp;
|
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.approvalTypes = at;
|
this.approvalTypes = at;
|
||||||
this.user = u;
|
this.user = u;
|
||||||
this.anonymous = au;
|
this.anonymous = au;
|
||||||
this.changeControlFactory = cf;
|
this.changeControlGenericFactory = gf;
|
||||||
this.patchSetInfoFactory = psi;
|
this.patchSetInfoFactory = psi;
|
||||||
this.patchListCache = plc;
|
this.patchListCache = plc;
|
||||||
this.sshInfo = sshInfo;
|
|
||||||
this.urlProvider = curl;
|
this.urlProvider = curl;
|
||||||
this.urls = urls;
|
this.urls = urls;
|
||||||
|
|
||||||
@ -187,99 +141,68 @@ public class ListChanges {
|
|||||||
options = EnumSet.noneOf(ListChangesOption.class);
|
options = EnumSet.noneOf(ListChangesOption.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public OutputFormat getFormat() {
|
public ChangeJson addOption(ListChangesOption o) {
|
||||||
return format;
|
options.add(o);
|
||||||
}
|
|
||||||
|
|
||||||
public ListChanges setFormat(OutputFormat fmt) {
|
|
||||||
this.format = fmt;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListChanges addQuery(String query) {
|
public ChangeJson addOptions(Collection<ListChangesOption> o) {
|
||||||
if (queries == null) {
|
options.addAll(o);
|
||||||
queries = Lists.newArrayList();
|
|
||||||
}
|
|
||||||
queries.add(query);
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void query(Writer out)
|
public ChangeJson setSshInfo(SshInfo info) {
|
||||||
throws OrmException, QueryParseException, IOException {
|
sshInfo = info;
|
||||||
if (imp.isDisabled()) {
|
return this;
|
||||||
throw new QueryParseException("query disabled");
|
}
|
||||||
}
|
|
||||||
if (queries == null || queries.isEmpty()) {
|
|
||||||
queries = Collections.singletonList("status:open");
|
|
||||||
} else if (queries.size() > 10) {
|
|
||||||
// Hard-code a default maximum number of queries to prevent
|
|
||||||
// users from submitting too much to the server in a single call.
|
|
||||||
throw new QueryParseException("limit of 10 queries");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(queries.size());
|
public ChangeJson setChangeControlFactory(ChangeControl.Factory cf) {
|
||||||
for (String query : queries) {
|
changeControlUserFactory = cf;
|
||||||
List<ChangeData> changes = imp.queryChanges(query);
|
return this;
|
||||||
boolean moreChanges = imp.getLimit() > 0 && changes.size() > imp.getLimit();
|
}
|
||||||
if (moreChanges) {
|
|
||||||
if (reverse) {
|
public ChangeInfo format(ChangeResource rsrc) throws OrmException {
|
||||||
changes = changes.subList(1, changes.size());
|
return format(new ChangeData(rsrc.getControl()));
|
||||||
} else {
|
}
|
||||||
changes = changes.subList(0, imp.getLimit());
|
|
||||||
}
|
public ChangeInfo format(Change change) throws OrmException {
|
||||||
}
|
return format(new ChangeData(change));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChangeInfo format(Change.Id id) throws OrmException {
|
||||||
|
return format(new ChangeData(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChangeInfo format(ChangeData cd) throws OrmException {
|
||||||
|
List<ChangeData> tmp = ImmutableList.of(cd);
|
||||||
|
return formatList2(ImmutableList.of(tmp)).get(0).get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<List<ChangeInfo>> formatList2(List<List<ChangeData>> in)
|
||||||
|
throws OrmException {
|
||||||
|
List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size());
|
||||||
|
for (List<ChangeData> changes : in) {
|
||||||
ChangeData.ensureChangeLoaded(db, changes);
|
ChangeData.ensureChangeLoaded(db, changes);
|
||||||
ChangeData.ensureCurrentPatchSetLoaded(db, changes);
|
ChangeData.ensureCurrentPatchSetLoaded(db, changes);
|
||||||
ChangeData.ensureCurrentApprovalsLoaded(db, changes);
|
ChangeData.ensureCurrentApprovalsLoaded(db, changes);
|
||||||
|
res.add(toChangeInfo(changes));
|
||||||
List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
|
|
||||||
for (ChangeData cd : changes) {
|
|
||||||
info.add(toChangeInfo(cd));
|
|
||||||
}
|
|
||||||
if (moreChanges && !info.isEmpty()) {
|
|
||||||
if (reverse) {
|
|
||||||
info.get(0)._moreChanges = true;
|
|
||||||
} else {
|
|
||||||
info.get(info.size() - 1)._moreChanges = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.add(info);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accounts.isEmpty()) {
|
if (!accounts.isEmpty()) {
|
||||||
for (Account account : db.get().accounts().get(accounts.keySet())) {
|
for (Account account : db.get().accounts().get(accounts.keySet())) {
|
||||||
AccountAttribute a = accounts.get(account.getId());
|
AccountAttribute a = accounts.get(account.getId());
|
||||||
a.name = Strings.emptyToNull(account.getFullName());
|
a.name = Strings.emptyToNull(account.getFullName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
if (format.isJson()) {
|
private List<ChangeInfo> toChangeInfo(List<ChangeData> changes)
|
||||||
format.newGson().toJson(
|
throws OrmException {
|
||||||
res.size() == 1 ? res.get(0) : res,
|
List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
|
||||||
new TypeToken<List<ChangeInfo>>() {}.getType(),
|
for (ChangeData cd : changes) {
|
||||||
out);
|
info.add(toChangeInfo(cd));
|
||||||
out.write('\n');
|
|
||||||
} else {
|
|
||||||
boolean firstQuery = true;
|
|
||||||
for (List<ChangeInfo> info : res) {
|
|
||||||
if (firstQuery) {
|
|
||||||
firstQuery = false;
|
|
||||||
} else {
|
|
||||||
out.write('\n');
|
|
||||||
}
|
|
||||||
for (ChangeInfo c : info) {
|
|
||||||
String id = new Change.Key(c.changeId).abbreviate();
|
|
||||||
String subject = c.subject;
|
|
||||||
if (subject.length() + id.length() > 80) {
|
|
||||||
subject = subject.substring(0, 80 - id.length());
|
|
||||||
}
|
|
||||||
out.write(id);
|
|
||||||
out.write(' ');
|
|
||||||
out.write(subject.replace('\n', ' '));
|
|
||||||
out.write('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChangeInfo toChangeInfo(ChangeData cd) throws OrmException {
|
private ChangeInfo toChangeInfo(ChangeData cd) throws OrmException {
|
||||||
@ -338,7 +261,11 @@ public class ListChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ctrl = changeControlFactory.controlFor(cd.change(db));
|
if (changeControlUserFactory != null) {
|
||||||
|
ctrl = changeControlUserFactory.controlFor(cd.change(db));
|
||||||
|
} else {
|
||||||
|
ctrl = changeControlGenericFactory.controlFor(cd.change(db), user);
|
||||||
|
}
|
||||||
} catch (NoSuchChangeException e) {
|
} catch (NoSuchChangeException e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -577,7 +504,7 @@ public class ListChanges {
|
|||||||
+ cd.change(db).getProject().get(), refName));
|
+ cd.change(db).getProject().get(), refName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!sshInfo.getHostKeys().isEmpty()) {
|
if (sshInfo != null && !sshInfo.getHostKeys().isEmpty()) {
|
||||||
HostKey host = sshInfo.getHostKeys().get(0);
|
HostKey host = sshInfo.getHostKeys().get(0);
|
||||||
r.put("ssh", new FetchInfo(String.format(
|
r.put("ssh", new FetchInfo(String.format(
|
||||||
"ssh://%s/%s",
|
"ssh://%s/%s",
|
||||||
@ -597,14 +524,14 @@ public class ListChanges {
|
|||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
static class ChangeInfo {
|
public static class ChangeInfo {
|
||||||
final String kind = "gerritcodereview#change";
|
final String kind = "gerritcodereview#change";
|
||||||
String id;
|
String id;
|
||||||
String project;
|
String project;
|
||||||
String branch;
|
String branch;
|
||||||
String topic;
|
String topic;
|
||||||
String changeId;
|
public String changeId;
|
||||||
String subject;
|
public String subject;
|
||||||
Change.Status status;
|
Change.Status status;
|
||||||
Timestamp created;
|
Timestamp created;
|
||||||
Timestamp updated;
|
Timestamp updated;
|
||||||
@ -618,8 +545,7 @@ public class ListChanges {
|
|||||||
Map<String, LabelInfo> labels;
|
Map<String, LabelInfo> labels;
|
||||||
String current_revision;
|
String current_revision;
|
||||||
Map<String, RevisionInfo> revisions;
|
Map<String, RevisionInfo> revisions;
|
||||||
|
public Boolean _moreChanges;
|
||||||
Boolean _moreChanges;
|
|
||||||
|
|
||||||
void finish() {
|
void finish() {
|
||||||
try {
|
try {
|
@ -0,0 +1,44 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.change;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.restapi.RestResource;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestView;
|
||||||
|
import com.google.gerrit.reviewdb.client.Change;
|
||||||
|
import com.google.gerrit.server.project.ChangeControl;
|
||||||
|
import com.google.inject.TypeLiteral;
|
||||||
|
|
||||||
|
public class ChangeResource implements RestResource {
|
||||||
|
public static final TypeLiteral<RestView<ChangeResource>> CHANGE_KIND =
|
||||||
|
new TypeLiteral<RestView<ChangeResource>>() {};
|
||||||
|
|
||||||
|
private final ChangeControl control;
|
||||||
|
|
||||||
|
public ChangeResource(ChangeControl control) {
|
||||||
|
this.control = control;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ChangeResource(ChangeResource copy) {
|
||||||
|
this.control = copy.control;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChangeControl getControl() {
|
||||||
|
return control;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Change getChange() {
|
||||||
|
return getControl().getChange();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,140 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.change;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||||
|
import com.google.gerrit.extensions.restapi.TopLevelResource;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestCollection;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestView;
|
||||||
|
import com.google.gerrit.reviewdb.client.Branch;
|
||||||
|
import com.google.gerrit.reviewdb.client.Change;
|
||||||
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
|
import com.google.gerrit.server.project.ChangeControl;
|
||||||
|
import com.google.gerrit.server.project.NoSuchChangeException;
|
||||||
|
import com.google.gerrit.server.query.change.QueryChanges;
|
||||||
|
import com.google.gwtorm.server.OrmException;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Provider;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lib.Constants;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ChangesCollection implements
|
||||||
|
RestCollection<TopLevelResource, ChangeResource> {
|
||||||
|
private final Provider<ReviewDb> db;
|
||||||
|
private final ChangeControl.Factory changeControlFactory;
|
||||||
|
private final Provider<QueryChanges> queryFactory;
|
||||||
|
private final DynamicMap<RestView<ChangeResource>> views;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ChangesCollection(
|
||||||
|
Provider<ReviewDb> dbProvider,
|
||||||
|
ChangeControl.Factory changeControlFactory,
|
||||||
|
Provider<QueryChanges> queryFactory,
|
||||||
|
DynamicMap<RestView<ChangeResource>> views) {
|
||||||
|
this.db = dbProvider;
|
||||||
|
this.changeControlFactory = changeControlFactory;
|
||||||
|
this.queryFactory = queryFactory;
|
||||||
|
this.views = views;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RestView<TopLevelResource> list() {
|
||||||
|
return queryFactory.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DynamicMap<RestView<ChangeResource>> views() {
|
||||||
|
return views;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChangeResource parse(TopLevelResource root, String id)
|
||||||
|
throws ResourceNotFoundException, OrmException,
|
||||||
|
UnsupportedEncodingException {
|
||||||
|
ParsedId p = new ParsedId(id);
|
||||||
|
List<Change> changes = findChanges(p);
|
||||||
|
if (changes.size() != 1) {
|
||||||
|
throw new ResourceNotFoundException(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangeControl control;
|
||||||
|
try {
|
||||||
|
control = changeControlFactory.validateFor(changes.get(0));
|
||||||
|
} catch (NoSuchChangeException e) {
|
||||||
|
throw new ResourceNotFoundException(id);
|
||||||
|
}
|
||||||
|
return new ChangeResource(control);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Change> findChanges(ParsedId k) throws OrmException {
|
||||||
|
if (k.legacyId != null) {
|
||||||
|
Change c = db.get().changes().get(k.legacyId);
|
||||||
|
if (c != null) {
|
||||||
|
return ImmutableList.of(c);
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
} else if (k.project == null && k.branch == null && k.changeId != null) {
|
||||||
|
return db.get().changes().byKey(new Change.Key(k.changeId)).toList();
|
||||||
|
}
|
||||||
|
return db.get().changes().byBranchKey(
|
||||||
|
k.branch(),
|
||||||
|
new Change.Key(k.changeId)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ParsedId {
|
||||||
|
Change.Id legacyId;
|
||||||
|
String project;
|
||||||
|
String branch;
|
||||||
|
String changeId;
|
||||||
|
|
||||||
|
ParsedId(String id) throws ResourceNotFoundException,
|
||||||
|
UnsupportedEncodingException {
|
||||||
|
if (id.matches("^[0-9]+$")) {
|
||||||
|
legacyId = Change.Id.parse(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int t2 = id.lastIndexOf('~');
|
||||||
|
int t1 = id.lastIndexOf('~', t2 - 1);
|
||||||
|
if (t1 < 0 || t2 < 0) {
|
||||||
|
if (!id.matches("^I[0-9a-z]{40}$")) {
|
||||||
|
throw new ResourceNotFoundException(id);
|
||||||
|
}
|
||||||
|
changeId = id;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
project = URLDecoder.decode(id.substring(0, t1), "UTF-8");
|
||||||
|
branch = URLDecoder.decode(id.substring(t1 + 1, t2), "UTF-8");
|
||||||
|
changeId = URLDecoder.decode(id.substring(t2 + 1), "UTF-8");
|
||||||
|
|
||||||
|
if (!branch.startsWith(Constants.R_REFS)) {
|
||||||
|
branch = Constants.R_HEADS + branch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Branch.NameKey branch() {
|
||||||
|
return new Branch.NameKey(new Project.NameKey(project), branch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.change;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||||
|
import com.google.gwtorm.server.OrmException;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
public class GetChange implements RestReadView<ChangeResource> {
|
||||||
|
private final ChangeJson json;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
GetChange(ChangeJson json) {
|
||||||
|
this.json = json;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object apply(ChangeResource rsrc) throws OrmException {
|
||||||
|
return json.format(rsrc);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.change;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||||
|
|
||||||
|
public class GetReviewer implements RestReadView<ReviewerResource> {
|
||||||
|
@Override
|
||||||
|
public Object apply(ReviewerResource resource)
|
||||||
|
throws BadRequestException, Exception {
|
||||||
|
throw new BadRequestException("Not yet implemented");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.change;
|
||||||
|
|
||||||
|
import static com.google.gerrit.server.change.ChangeResource.CHANGE_KIND;
|
||||||
|
import static com.google.gerrit.server.change.ReviewerResource.REVIEWER_KIND;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestApiModule;
|
||||||
|
|
||||||
|
public class Module extends RestApiModule {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
DynamicMap.mapOf(binder(), CHANGE_KIND);
|
||||||
|
DynamicMap.mapOf(binder(), REVIEWER_KIND);
|
||||||
|
|
||||||
|
get(CHANGE_KIND).to(GetChange.class);
|
||||||
|
post(CHANGE_KIND, "abandon").to(Abandon.class);
|
||||||
|
child(CHANGE_KIND, "reviewers").to(Reviewers.class);
|
||||||
|
|
||||||
|
get(REVIEWER_KIND).to(GetReviewer.class);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.change;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.restapi.RestView;
|
||||||
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
|
import com.google.inject.TypeLiteral;
|
||||||
|
|
||||||
|
public class ReviewerResource extends ChangeResource {
|
||||||
|
public static final TypeLiteral<RestView<ReviewerResource>> REVIEWER_KIND =
|
||||||
|
new TypeLiteral<RestView<ReviewerResource>>() {};
|
||||||
|
|
||||||
|
private final Account.Id id;
|
||||||
|
|
||||||
|
public ReviewerResource(ChangeResource rsrc, Account.Id id) {
|
||||||
|
super(rsrc);
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Account.Id getAccountId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.change;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||||
|
import com.google.gerrit.extensions.restapi.ChildCollection;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestView;
|
||||||
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
public class Reviewers implements
|
||||||
|
ChildCollection<ChangeResource, ReviewerResource> {
|
||||||
|
private final DynamicMap<RestView<ReviewerResource>> views;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Reviewers(DynamicMap<RestView<ReviewerResource>> views) {
|
||||||
|
this.views = views;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DynamicMap<RestView<ReviewerResource>> views() {
|
||||||
|
return views;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RestView<ChangeResource> list() throws ResourceNotFoundException {
|
||||||
|
throw new ResourceNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReviewerResource parse(ChangeResource change, String id)
|
||||||
|
throws ResourceNotFoundException, Exception {
|
||||||
|
if (id.matches("^[0-9]+$")) {
|
||||||
|
return new ReviewerResource(change, Account.Id.parse(id));
|
||||||
|
}
|
||||||
|
throw new ResourceNotFoundException(id);
|
||||||
|
}
|
||||||
|
}
|
@ -38,102 +38,15 @@ import org.kohsuke.args4j.Argument;
|
|||||||
import org.kohsuke.args4j.Option;
|
import org.kohsuke.args4j.Option;
|
||||||
|
|
||||||
public class AbandonChange implements Callable<ReviewResult> {
|
public class AbandonChange implements Callable<ReviewResult> {
|
||||||
|
|
||||||
private final AbandonedSender.Factory abandonedSenderFactory;
|
|
||||||
private final ChangeControl.Factory changeControlFactory;
|
|
||||||
private final ReviewDb db;
|
|
||||||
private final IdentifiedUser currentUser;
|
|
||||||
private final ChangeHooks hooks;
|
|
||||||
|
|
||||||
@Argument(index = 0, required = true, multiValued = false, usage = "change to abandon")
|
|
||||||
private Change.Id changeId;
|
|
||||||
|
|
||||||
public void setChangeId(final Change.Id changeId) {
|
public void setChangeId(final Change.Id changeId) {
|
||||||
this.changeId = changeId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Option(name = "--message", aliases = {"-m"},
|
|
||||||
usage = "optional message to append to change")
|
|
||||||
private String message;
|
|
||||||
|
|
||||||
public void setMessage(final String message) {
|
public void setMessage(final String message) {
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
AbandonChange(final AbandonedSender.Factory abandonedSenderFactory,
|
|
||||||
final ChangeControl.Factory changeControlFactory, final ReviewDb db,
|
|
||||||
final IdentifiedUser currentUser, final ChangeHooks hooks) {
|
|
||||||
this.abandonedSenderFactory = abandonedSenderFactory;
|
|
||||||
this.changeControlFactory = changeControlFactory;
|
|
||||||
this.db = db;
|
|
||||||
this.currentUser = currentUser;
|
|
||||||
this.hooks = hooks;
|
|
||||||
|
|
||||||
changeId = null;
|
|
||||||
message = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ReviewResult call() throws EmailException,
|
public ReviewResult call() throws EmailException,
|
||||||
InvalidChangeOperationException, NoSuchChangeException, OrmException {
|
InvalidChangeOperationException, NoSuchChangeException, OrmException {
|
||||||
if (changeId == null) {
|
throw new UnsupportedOperationException();
|
||||||
throw new InvalidChangeOperationException("changeId is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
final ReviewResult result = new ReviewResult();
|
|
||||||
result.setChangeId(changeId);
|
|
||||||
|
|
||||||
final ChangeControl control = changeControlFactory.validateFor(changeId);
|
|
||||||
final Change change = db.changes().get(changeId);
|
|
||||||
final PatchSet.Id patchSetId = change.currentPatchSetId();
|
|
||||||
final PatchSet patch = db.patchSets().get(patchSetId);
|
|
||||||
if (!control.canAbandon()) {
|
|
||||||
result.addError(new ReviewResult.Error(
|
|
||||||
ReviewResult.Error.Type.ABANDON_NOT_PERMITTED));
|
|
||||||
} else if (patch == null) {
|
|
||||||
throw new NoSuchChangeException(changeId);
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Create a message to accompany the abandoned change
|
|
||||||
final ChangeMessage cmsg = new ChangeMessage(
|
|
||||||
new ChangeMessage.Key(changeId, ChangeUtil.messageUUID(db)),
|
|
||||||
currentUser.getAccountId(), patchSetId);
|
|
||||||
final StringBuilder msgBuf =
|
|
||||||
new StringBuilder("Patch Set " + patchSetId.get() + ": Abandoned");
|
|
||||||
if (message != null && message.length() > 0) {
|
|
||||||
msgBuf.append("\n\n");
|
|
||||||
msgBuf.append(message);
|
|
||||||
}
|
|
||||||
cmsg.setMessage(msgBuf.toString());
|
|
||||||
|
|
||||||
// Abandon the change
|
|
||||||
final Change updatedChange = db.changes().atomicUpdate(changeId,
|
|
||||||
new AtomicUpdate<Change>() {
|
|
||||||
@Override
|
|
||||||
public Change update(Change change) {
|
|
||||||
if (change.getStatus().isOpen()) {
|
|
||||||
change.setStatus(Change.Status.ABANDONED);
|
|
||||||
ChangeUtil.updated(change);
|
|
||||||
return change;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (updatedChange == null) {
|
|
||||||
result.addError(new ReviewResult.Error(
|
|
||||||
ReviewResult.Error.Type.CHANGE_IS_CLOSED));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChangeUtil.updatedChange(db, currentUser, updatedChange, cmsg,
|
|
||||||
abandonedSenderFactory);
|
|
||||||
hooks.doChangeAbandonedHook(updatedChange, currentUser.getAccount(),
|
|
||||||
message, db);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -183,6 +183,9 @@ public class GerritGlobalModule extends FactoryModule {
|
|||||||
factory(FunctionState.Factory.class);
|
factory(FunctionState.Factory.class);
|
||||||
|
|
||||||
install(new AuditModule());
|
install(new AuditModule());
|
||||||
|
install(new com.google.gerrit.server.account.Module());
|
||||||
|
install(new com.google.gerrit.server.change.Module());
|
||||||
|
install(new com.google.gerrit.server.project.Module());
|
||||||
|
|
||||||
bind(GitReferenceUpdated.class);
|
bind(GitReferenceUpdated.class);
|
||||||
DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
|
DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
|
||||||
|
@ -29,7 +29,6 @@ import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
|
|||||||
import com.google.gerrit.server.changedetail.PublishDraft;
|
import com.google.gerrit.server.changedetail.PublishDraft;
|
||||||
import com.google.gerrit.server.changedetail.RebaseChange;
|
import com.google.gerrit.server.changedetail.RebaseChange;
|
||||||
import com.google.gerrit.server.changedetail.Submit;
|
import com.google.gerrit.server.changedetail.Submit;
|
||||||
import com.google.gerrit.server.dashboard.ListDashboards;
|
|
||||||
import com.google.gerrit.server.git.AsyncReceiveCommits;
|
import com.google.gerrit.server.git.AsyncReceiveCommits;
|
||||||
import com.google.gerrit.server.git.BanCommit;
|
import com.google.gerrit.server.git.BanCommit;
|
||||||
import com.google.gerrit.server.git.CreateCodeReviewNotes;
|
import com.google.gerrit.server.git.CreateCodeReviewNotes;
|
||||||
@ -66,7 +65,6 @@ public class GerritRequestModule extends FactoryModule {
|
|||||||
bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
|
bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
|
||||||
bind(MetaDataUpdate.User.class).in(RequestScoped.class);
|
bind(MetaDataUpdate.User.class).in(RequestScoped.class);
|
||||||
bind(ListProjects.class);
|
bind(ListProjects.class);
|
||||||
bind(ListDashboards.class);
|
|
||||||
bind(ApprovalsUtil.class);
|
bind(ApprovalsUtil.class);
|
||||||
|
|
||||||
bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
|
bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
|
||||||
|
@ -1,400 +0,0 @@
|
|||||||
// Copyright (C) 2012 The Android Open Source Project
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package com.google.gerrit.server.dashboard;
|
|
||||||
|
|
||||||
import com.google.common.collect.Maps;
|
|
||||||
import com.google.gerrit.reviewdb.client.Project;
|
|
||||||
import com.google.gerrit.server.CurrentUser;
|
|
||||||
import com.google.gerrit.server.OutputFormat;
|
|
||||||
import com.google.gerrit.server.config.AllProjectsName;
|
|
||||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
|
||||||
import com.google.gerrit.server.project.ProjectCache;
|
|
||||||
import com.google.gerrit.server.project.ProjectControl;
|
|
||||||
import com.google.gerrit.server.project.ProjectState;
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
|
|
||||||
import org.eclipse.jgit.errors.ConfigInvalidException;
|
|
||||||
import org.eclipse.jgit.lib.Config;
|
|
||||||
import org.eclipse.jgit.lib.ObjectLoader;
|
|
||||||
import org.eclipse.jgit.lib.Ref;
|
|
||||||
import org.eclipse.jgit.lib.Repository;
|
|
||||||
import org.eclipse.jgit.revwalk.RevCommit;
|
|
||||||
import org.eclipse.jgit.revwalk.RevTree;
|
|
||||||
import org.eclipse.jgit.revwalk.RevWalk;
|
|
||||||
import org.eclipse.jgit.treewalk.TreeWalk;
|
|
||||||
import org.eclipse.jgit.treewalk.filter.PathFilter;
|
|
||||||
import org.kohsuke.args4j.Option;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.BufferedWriter;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.StringTokenizer;
|
|
||||||
|
|
||||||
/** List projects visible to the calling user. */
|
|
||||||
public class ListDashboards {
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ListDashboards.class);
|
|
||||||
private static String REFS_DASHBOARDS = "refs/meta/dashboards/";
|
|
||||||
|
|
||||||
public static enum Level {
|
|
||||||
PROJECT
|
|
||||||
};
|
|
||||||
|
|
||||||
private final CurrentUser currentUser;
|
|
||||||
private final ProjectCache projectCache;
|
|
||||||
private final GitRepositoryManager repoManager;
|
|
||||||
private AllProjectsName allProjects;
|
|
||||||
|
|
||||||
@Option(name = "--format", metaVar = "FMT", usage = "Output display format")
|
|
||||||
private OutputFormat format = OutputFormat.JSON;
|
|
||||||
|
|
||||||
@Option(name = "--default", usage = "only the projects default dashboard is returned")
|
|
||||||
private boolean defaultDashboard;
|
|
||||||
|
|
||||||
private Level level;
|
|
||||||
private String entityName;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
protected ListDashboards(CurrentUser currentUser, ProjectCache projectCache,
|
|
||||||
GitRepositoryManager repoManager, AllProjectsName allProjects) {
|
|
||||||
this.currentUser = currentUser;
|
|
||||||
this.projectCache = projectCache;
|
|
||||||
this.repoManager = repoManager;
|
|
||||||
this.allProjects = allProjects;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OutputFormat getFormat() {
|
|
||||||
return format;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ListDashboards setFormat(OutputFormat fmt) {
|
|
||||||
if (!format.isJson()) {
|
|
||||||
throw new IllegalArgumentException(format.name() + " not supported");
|
|
||||||
}
|
|
||||||
this.format = fmt;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ListDashboards setLevel(Level level) {
|
|
||||||
this.level = level;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ListDashboards setEntityName(String entityName) {
|
|
||||||
this.entityName = entityName;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void display(OutputStream out) {
|
|
||||||
final PrintWriter stdout;
|
|
||||||
try {
|
|
||||||
stdout = new PrintWriter(new BufferedWriter(new OutputStreamWriter(out, "UTF-8")));
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
// Our encoding is required by the specifications for the runtime.
|
|
||||||
throw new RuntimeException("JVM lacks UTF-8 encoding", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final Map<String, DashboardInfo> dashboards;
|
|
||||||
if (level != null) {
|
|
||||||
switch (level) {
|
|
||||||
case PROJECT:
|
|
||||||
final Project.NameKey projectName = new Project.NameKey(entityName);
|
|
||||||
final ProjectState projectState = projectCache.get(projectName);
|
|
||||||
DashboardInfo defaultInfo = findProjectDefaultDashboard(projectState);
|
|
||||||
if (defaultDashboard) {
|
|
||||||
dashboards = Maps.newTreeMap();
|
|
||||||
if (defaultInfo != null) {
|
|
||||||
dashboards.put(defaultInfo.id, defaultInfo);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dashboards =
|
|
||||||
allDashboardsFor(projectState, defaultInfo != null
|
|
||||||
? defaultInfo.id : null);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException("unsupported dashboard level: " + level);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dashboards = Maps.newTreeMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
format.newGson().toJson(dashboards,
|
|
||||||
new TypeToken<Map<String, DashboardInfo>>() {}.getType(), stdout);
|
|
||||||
stdout.print('\n');
|
|
||||||
} finally {
|
|
||||||
stdout.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, DashboardInfo> allDashboardsFor(ProjectState projectState,
|
|
||||||
final String defaultId) {
|
|
||||||
final Project.NameKey projectName = projectState.getProject().getNameKey();
|
|
||||||
Project.NameKey parent;
|
|
||||||
|
|
||||||
Map<String, DashboardInfo> dashboards = Maps.newTreeMap();
|
|
||||||
Set<Project.NameKey> seen = new HashSet<Project.NameKey>();
|
|
||||||
seen.add(projectName);
|
|
||||||
do {
|
|
||||||
dashboards = addProjectDashboards(projectState, dashboards, defaultId);
|
|
||||||
|
|
||||||
parent = projectState.getProject().getParent(allProjects);
|
|
||||||
projectState = projectCache.get(parent);
|
|
||||||
} while (projectState != null && seen.add(parent));
|
|
||||||
|
|
||||||
for (String id : dashboards.keySet()) {
|
|
||||||
replaceTokens(dashboards.get(id), projectName.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
return dashboards;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void replaceTokens(DashboardInfo info, String project) {
|
|
||||||
info.parameters = info.parameters.replaceAll("[$][{]project[}]", project);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, DashboardInfo> addProjectDashboards(
|
|
||||||
final ProjectState projectState, Map<String, DashboardInfo> all,
|
|
||||||
final String defaultId) {
|
|
||||||
final Map<String, DashboardInfo> dashboards = projectDashboards(projectState,
|
|
||||||
defaultId);
|
|
||||||
dashboards.putAll(all);
|
|
||||||
return dashboards;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, DashboardInfo> projectDashboards(
|
|
||||||
final ProjectState projectState, final String defaultId) {
|
|
||||||
final Map<String, DashboardInfo> dashboards = Maps.newTreeMap();
|
|
||||||
|
|
||||||
final ProjectControl projectControl = projectState.controlFor(currentUser);
|
|
||||||
if (projectState == null || !projectControl.isVisible()) {
|
|
||||||
return dashboards;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Project.NameKey projectName = projectState.getProject().getNameKey();
|
|
||||||
Repository repo = null;
|
|
||||||
RevWalk revWalk = null;
|
|
||||||
try {
|
|
||||||
repo = repoManager.openRepository(projectName);
|
|
||||||
revWalk = new RevWalk(repo);
|
|
||||||
final Map<String, Ref> refs = repo.getRefDatabase().getRefs(REFS_DASHBOARDS);
|
|
||||||
for (final Ref ref : refs.values()) {
|
|
||||||
if (projectControl.controlForRef(ref.getName()).canRead()) {
|
|
||||||
dashboards.putAll(loadDashboards(projectControl.getProject(), repo,
|
|
||||||
revWalk, ref, defaultId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.warn("Failed to load dashboards of project " + projectName.get(), e);
|
|
||||||
} finally {
|
|
||||||
if (revWalk != null) {
|
|
||||||
revWalk.release();
|
|
||||||
}
|
|
||||||
if (repo != null) {
|
|
||||||
repo.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dashboards;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, DashboardInfo> loadDashboards(final Project project,
|
|
||||||
final Repository repo, final RevWalk revWalk, final Ref ref,
|
|
||||||
final String defaultId) throws IOException {
|
|
||||||
final Map<String, DashboardInfo> dashboards = Maps.newTreeMap();
|
|
||||||
TreeWalk treeWalk = new TreeWalk(repo);
|
|
||||||
try {
|
|
||||||
final RevCommit commit = revWalk.parseCommit(ref.getObjectId());
|
|
||||||
final RevTree tree = commit.getTree();
|
|
||||||
treeWalk.addTree(tree);
|
|
||||||
treeWalk.setRecursive(true);
|
|
||||||
while (treeWalk.next()) {
|
|
||||||
|
|
||||||
final ObjectLoader loader = repo.open(treeWalk.getObjectId(0));
|
|
||||||
final DashboardInfo info =
|
|
||||||
loadDashboard(project, ref.getName(), treeWalk.getPathString(),
|
|
||||||
defaultId, loader);
|
|
||||||
dashboards.put(info.id, info);
|
|
||||||
}
|
|
||||||
} catch (ConfigInvalidException e) {
|
|
||||||
log.warn("Failed to load dashboards of project " + project.getName()
|
|
||||||
+ " from ref " + ref.getName(), e);
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.warn("Failed to load dashboards of project " + project.getName()
|
|
||||||
+ " from ref " + ref.getName(), e);
|
|
||||||
} finally {
|
|
||||||
treeWalk.release();
|
|
||||||
}
|
|
||||||
return dashboards;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DashboardInfo findProjectDefaultDashboard(ProjectState projectState) {
|
|
||||||
final Project.NameKey projectName = projectState.getProject().getNameKey();
|
|
||||||
Project.NameKey parent;
|
|
||||||
|
|
||||||
DashboardInfo info;
|
|
||||||
Set<Project.NameKey> seen = new HashSet<Project.NameKey>();
|
|
||||||
seen.add(projectName);
|
|
||||||
boolean considerLocal = true;
|
|
||||||
do {
|
|
||||||
info = loadProjectDefaultDashboard(projectState, considerLocal);
|
|
||||||
if (info != null) {
|
|
||||||
replaceTokens(info, projectName.get());
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
considerLocal = false;
|
|
||||||
|
|
||||||
parent = projectState.getProject().getParent(allProjects);
|
|
||||||
projectState = projectCache.get(parent);
|
|
||||||
} while (projectState != null && seen.add(parent));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DashboardInfo loadProjectDefaultDashboard(final ProjectState projectState,
|
|
||||||
boolean considerLocal) {
|
|
||||||
final ProjectControl projectControl = projectState.controlFor(currentUser);
|
|
||||||
if (projectState == null || !projectControl.isVisible()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Project project = projectControl.getProject();
|
|
||||||
String defaultDashboardId = project.getDefaultDashboard();
|
|
||||||
if (considerLocal && project.getLocalDefaultDashboard() != null) {
|
|
||||||
defaultDashboardId = project.getLocalDefaultDashboard();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defaultDashboardId == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return loadDashboard(projectControl, defaultDashboardId, defaultDashboardId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DashboardInfo loadDashboard(final ProjectControl projectControl,
|
|
||||||
final String dashboardId, final String defaultId) {
|
|
||||||
StringTokenizer t = new StringTokenizer(dashboardId, ":");
|
|
||||||
if (t.countTokens() != 2) {
|
|
||||||
throw new IllegalStateException("failed to load dashboard, invalid dashboard id: " + dashboardId);
|
|
||||||
}
|
|
||||||
final String refName = t.nextToken();
|
|
||||||
final String path = t.nextToken();
|
|
||||||
|
|
||||||
Repository repo = null;
|
|
||||||
RevWalk revWalk = null;
|
|
||||||
TreeWalk treeWalk = null;
|
|
||||||
try {
|
|
||||||
repo =
|
|
||||||
repoManager.openRepository(projectControl.getProject().getNameKey());
|
|
||||||
final Ref ref = repo.getRef(refName);
|
|
||||||
if (ref == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!projectControl.controlForRef(ref.getName()).canRead()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
revWalk = new RevWalk(repo);
|
|
||||||
final RevCommit commit = revWalk.parseCommit(ref.getObjectId());
|
|
||||||
treeWalk = new TreeWalk(repo);
|
|
||||||
treeWalk.addTree(commit.getTree());
|
|
||||||
treeWalk.setRecursive(true);
|
|
||||||
treeWalk.setFilter(PathFilter.create(path));
|
|
||||||
if (!treeWalk.next()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final ObjectLoader loader = repo.open(treeWalk.getObjectId(0));
|
|
||||||
return loadDashboard(projectControl.getProject(), refName, path,
|
|
||||||
defaultId, loader);
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.warn("Failed to load default dashboard", e);
|
|
||||||
} catch (ConfigInvalidException e) {
|
|
||||||
log.warn("Failed to load dashboards of project "
|
|
||||||
+ projectControl.getProject().getName() + " from ref " + refName, e);
|
|
||||||
} finally {
|
|
||||||
if (treeWalk != null) {
|
|
||||||
treeWalk.release();
|
|
||||||
}
|
|
||||||
if (revWalk != null) {
|
|
||||||
revWalk.release();
|
|
||||||
}
|
|
||||||
if (repo != null) {
|
|
||||||
repo.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DashboardInfo loadDashboard(final Project project, final String refName,
|
|
||||||
final String path, final String defaultId, final ObjectLoader loader)
|
|
||||||
throws IOException, ConfigInvalidException {
|
|
||||||
DashboardInfo info = new DashboardInfo();
|
|
||||||
info.dashboardName = path;
|
|
||||||
info.section = refName.substring(REFS_DASHBOARDS.length());
|
|
||||||
info.refName = refName;
|
|
||||||
info.projectName = project.getName();
|
|
||||||
info.id = createId(info.refName, info.dashboardName);
|
|
||||||
info.isDefault = info.id.equals(defaultId);
|
|
||||||
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
||||||
loader.copyTo(out);
|
|
||||||
Config dashboardConfig = new Config();
|
|
||||||
dashboardConfig.fromText(new String(out.toByteArray(), "UTF-8"));
|
|
||||||
|
|
||||||
info.description = dashboardConfig.getString("main", null, "description");
|
|
||||||
|
|
||||||
final StringBuilder query = new StringBuilder();
|
|
||||||
query.append("title=");
|
|
||||||
query.append(info.dashboardName.replaceAll(" ", "+"));
|
|
||||||
final Set<String> sections = dashboardConfig.getSubsections("section");
|
|
||||||
for (final String section : sections) {
|
|
||||||
query.append("&");
|
|
||||||
query.append(section.replaceAll(" ", "+"));
|
|
||||||
query.append("=");
|
|
||||||
query.append(dashboardConfig.getString("section", section, "query"));
|
|
||||||
}
|
|
||||||
info.parameters = query.toString();
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String createId(final String refName,
|
|
||||||
final String dashboardName) {
|
|
||||||
return refName + ":" + dashboardName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private static class DashboardInfo {
|
|
||||||
final String kind = "gerritcodereview#dashboard";
|
|
||||||
String id;
|
|
||||||
String dashboardName;
|
|
||||||
String section;
|
|
||||||
String refName;
|
|
||||||
String projectName;
|
|
||||||
String description;
|
|
||||||
String parameters;
|
|
||||||
boolean isDefault;
|
|
||||||
}
|
|
||||||
}
|
|
@ -40,6 +40,9 @@ public interface GitRepositoryManager {
|
|||||||
/** Configuration settings for a project {@code refs/meta/config} */
|
/** Configuration settings for a project {@code refs/meta/config} */
|
||||||
public static final String REF_CONFIG = "refs/meta/config";
|
public static final String REF_CONFIG = "refs/meta/config";
|
||||||
|
|
||||||
|
/** Configurations of project-specific dashboards (canned search queries). */
|
||||||
|
public static String REFS_DASHBOARDS = "refs/meta/dashboards/";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prefix applied to merge commit base nodes.
|
* Prefix applied to merge commit base nodes.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -106,6 +106,12 @@ public class MetaDataUpdate {
|
|||||||
getCommitBuilder().setMessage(message);
|
getCommitBuilder().setMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setAuthor(IdentifiedUser user) {
|
||||||
|
getCommitBuilder().setAuthor(user.newCommitterIdent(
|
||||||
|
getCommitBuilder().getCommitter().getWhen(),
|
||||||
|
getCommitBuilder().getCommitter().getTimeZone()));
|
||||||
|
}
|
||||||
|
|
||||||
/** Close the cached Repository handle. */
|
/** Close the cached Repository handle. */
|
||||||
public void close() {
|
public void close() {
|
||||||
getRepository().close();
|
getRepository().close();
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.plugins;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.gerrit.common.data.GlobalCapability;
|
||||||
|
import com.google.gerrit.extensions.annotations.RequiresCapability;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||||
|
import com.google.gerrit.server.plugins.DisablePlugin.Input;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
|
||||||
|
class DisablePlugin implements RestModifyView<PluginResource, Input> {
|
||||||
|
static class Input {
|
||||||
|
}
|
||||||
|
|
||||||
|
private final PluginLoader loader;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
DisablePlugin(PluginLoader loader) {
|
||||||
|
this.loader = loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<Input> inputType() {
|
||||||
|
return Input.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object apply(PluginResource resource, Input input) {
|
||||||
|
String name = resource.getName();
|
||||||
|
loader.disablePlugins(ImmutableSet.of(name));
|
||||||
|
return new ListPlugins.PluginInfo(loader.get(name));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.plugins;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.gerrit.common.data.GlobalCapability;
|
||||||
|
import com.google.gerrit.extensions.annotations.RequiresCapability;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||||
|
import com.google.gerrit.server.plugins.EnablePlugin.Input;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
|
||||||
|
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
|
||||||
|
class EnablePlugin implements RestModifyView<PluginResource, Input> {
|
||||||
|
static class Input {
|
||||||
|
}
|
||||||
|
|
||||||
|
private final PluginLoader loader;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
EnablePlugin(PluginLoader loader) {
|
||||||
|
this.loader = loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<Input> inputType() {
|
||||||
|
return Input.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object apply(PluginResource resource, Input input)
|
||||||
|
throws ResourceConflictException {
|
||||||
|
String name = resource.getName();
|
||||||
|
try {
|
||||||
|
loader.enablePlugins(ImmutableSet.of(name));
|
||||||
|
} catch (PluginInstallException e) {
|
||||||
|
StringWriter buf = new StringWriter();
|
||||||
|
buf.write(String.format("cannot enable %s\n", name));
|
||||||
|
PrintWriter pw = new PrintWriter(buf);
|
||||||
|
e.printStackTrace(pw);
|
||||||
|
pw.flush();
|
||||||
|
throw new ResourceConflictException(buf.toString());
|
||||||
|
}
|
||||||
|
return new ListPlugins.PluginInfo(loader.get(name));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.plugins;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||||
|
|
||||||
|
class GetStatus implements RestReadView<PluginResource> {
|
||||||
|
@Override
|
||||||
|
public Object apply(PluginResource resource) {
|
||||||
|
return new ListPlugins.PluginInfo(resource.getPlugin());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.plugins;
|
||||||
|
|
||||||
|
import com.google.gerrit.common.data.GlobalCapability;
|
||||||
|
import com.google.gerrit.extensions.annotations.RequiresCapability;
|
||||||
|
import com.google.gerrit.extensions.restapi.AuthException;
|
||||||
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
|
import com.google.gerrit.extensions.restapi.DefaultInput;
|
||||||
|
import com.google.gerrit.extensions.restapi.PutInput;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||||
|
import com.google.gerrit.extensions.restapi.TopLevelResource;
|
||||||
|
import com.google.gerrit.server.plugins.InstallPlugin.Input;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.zip.ZipException;
|
||||||
|
|
||||||
|
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
|
||||||
|
class InstallPlugin implements RestModifyView<TopLevelResource, Input> {
|
||||||
|
static class Input {
|
||||||
|
@DefaultInput
|
||||||
|
String url;
|
||||||
|
PutInput raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final PluginLoader loader;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
InstallPlugin(PluginLoader loader, String name) {
|
||||||
|
this.loader = loader;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<Input> inputType() {
|
||||||
|
return Input.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object apply(TopLevelResource resource, Input input)
|
||||||
|
throws AuthException, BadRequestException, ResourceConflictException,
|
||||||
|
Exception {
|
||||||
|
try {
|
||||||
|
InputStream in;
|
||||||
|
if (input.raw != null) {
|
||||||
|
in = input.raw.getInputStream();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
in = new URL(input.url).openStream();
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
throw new BadRequestException(e.getMessage());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new BadRequestException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
loader.installPluginFromStream(name, in);
|
||||||
|
} finally {
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
} catch (PluginInstallException e) {
|
||||||
|
StringWriter buf = new StringWriter();
|
||||||
|
buf.write(String.format("cannot install %s", name));
|
||||||
|
if (e.getCause() instanceof ZipException) {
|
||||||
|
buf.write(": ");
|
||||||
|
buf.write(e.getCause().getMessage());
|
||||||
|
} else {
|
||||||
|
buf.write(":\n");
|
||||||
|
PrintWriter pw = new PrintWriter(buf);
|
||||||
|
e.printStackTrace(pw);
|
||||||
|
pw.flush();
|
||||||
|
}
|
||||||
|
throw new BadRequestException(buf.toString());
|
||||||
|
}
|
||||||
|
return new ListPlugins.PluginInfo(loader.get(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
|
||||||
|
static class Overwrite implements RestModifyView<PluginResource, Input> {
|
||||||
|
private final PluginLoader loader;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Overwrite(PluginLoader loader) {
|
||||||
|
this.loader = loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<Input> inputType() {
|
||||||
|
return Input.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object apply(PluginResource resource, Input input)
|
||||||
|
throws AuthException, BadRequestException, ResourceConflictException,
|
||||||
|
Exception {
|
||||||
|
return new InstallPlugin(loader, resource.getName())
|
||||||
|
.apply(TopLevelResource.INSTANCE, input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,7 +17,13 @@ package com.google.gerrit.server.plugins;
|
|||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
|
import com.google.gerrit.extensions.restapi.AuthException;
|
||||||
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||||
|
import com.google.gerrit.extensions.restapi.TopLevelResource;
|
||||||
import com.google.gerrit.server.OutputFormat;
|
import com.google.gerrit.server.OutputFormat;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
@ -28,16 +34,18 @@ import java.io.OutputStream;
|
|||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/** List the installed plugins. */
|
/** List the installed plugins. */
|
||||||
public class ListPlugins {
|
public class ListPlugins implements RestReadView<TopLevelResource> {
|
||||||
private final PluginLoader pluginLoader;
|
private final PluginLoader pluginLoader;
|
||||||
|
|
||||||
@Option(name = "--format", metaVar = "FMT", usage = "Output display format")
|
@Deprecated
|
||||||
|
@Option(name = "--format", usage = "(deprecated) output format")
|
||||||
private OutputFormat format = OutputFormat.TEXT;
|
private OutputFormat format = OutputFormat.TEXT;
|
||||||
|
|
||||||
@Option(name = "--all", aliases = {"-a"}, usage = "List all plugins, including disabled plugins")
|
@Option(name = "--all", aliases = {"-a"}, usage = "List all plugins, including disabled plugins")
|
||||||
@ -57,19 +65,26 @@ public class ListPlugins {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void display(OutputStream out) {
|
@Override
|
||||||
final PrintWriter stdout;
|
public Object apply(TopLevelResource resource) throws AuthException,
|
||||||
try {
|
BadRequestException, ResourceConflictException, Exception {
|
||||||
stdout =
|
format = OutputFormat.JSON;
|
||||||
new PrintWriter(new BufferedWriter(new OutputStreamWriter(out,
|
return display(null);
|
||||||
"UTF-8")));
|
}
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
// Our encoding is required by the specifications for the runtime.
|
public JsonElement display(OutputStream displayOutputStream)
|
||||||
throw new RuntimeException("JVM lacks UTF-8 encoding", e);
|
throws UnsupportedEncodingException {
|
||||||
|
PrintWriter stdout = null;
|
||||||
|
if (displayOutputStream != null) {
|
||||||
|
try {
|
||||||
|
stdout = new PrintWriter(new BufferedWriter(
|
||||||
|
new OutputStreamWriter(displayOutputStream, "UTF-8")));
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("JVM lacks UTF-8 encoding", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, PluginInfo> output = Maps.newTreeMap();
|
Map<String, PluginInfo> output = Maps.newTreeMap();
|
||||||
|
|
||||||
List<Plugin> plugins = Lists.newArrayList(pluginLoader.getPlugins(all));
|
List<Plugin> plugins = Lists.newArrayList(pluginLoader.getPlugins(all));
|
||||||
Collections.sort(plugins, new Comparator<Plugin>() {
|
Collections.sort(plugins, new Comparator<Plugin>() {
|
||||||
@Override
|
@Override
|
||||||
@ -80,15 +95,11 @@ public class ListPlugins {
|
|||||||
|
|
||||||
if (!format.isJson()) {
|
if (!format.isJson()) {
|
||||||
stdout.format("%-30s %-10s %-8s\n", "Name", "Version", "Status");
|
stdout.format("%-30s %-10s %-8s\n", "Name", "Version", "Status");
|
||||||
stdout
|
stdout.print("-------------------------------------------------------------------------------\n");
|
||||||
.print("-------------------------------------------------------------------------------\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Plugin p : plugins) {
|
for (Plugin p : plugins) {
|
||||||
PluginInfo info = new PluginInfo();
|
PluginInfo info = new PluginInfo(p);
|
||||||
info.version = p.getVersion();
|
|
||||||
info.disabled = p.isDisabled() ? true : null;
|
|
||||||
|
|
||||||
if (format.isJson()) {
|
if (format.isJson()) {
|
||||||
output.put(p.getName(), info);
|
output.put(p.getName(), info);
|
||||||
} else {
|
} else {
|
||||||
@ -98,19 +109,34 @@ public class ListPlugins {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format.isJson()) {
|
if (stdout == null) {
|
||||||
|
return OutputFormat.JSON.newGson().toJsonTree(
|
||||||
|
output,
|
||||||
|
new TypeToken<Map<String, Object>>() {}.getType());
|
||||||
|
} else if (format.isJson()) {
|
||||||
format.newGson().toJson(output,
|
format.newGson().toJson(output,
|
||||||
new TypeToken<Map<String, PluginInfo>>() {}.getType(), stdout);
|
new TypeToken<Map<String, PluginInfo>>() {}.getType(), stdout);
|
||||||
stdout.print('\n');
|
stdout.print('\n');
|
||||||
}
|
}
|
||||||
stdout.flush();
|
stdout.flush();
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class PluginInfo {
|
static class PluginInfo {
|
||||||
|
final String kind = "gerritcodereview#plugin";
|
||||||
|
String id;
|
||||||
String version;
|
String version;
|
||||||
// disabled is only read via reflection when building the json output. We
|
|
||||||
// do not want to show a compiler error that it isn't used.
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
Boolean disabled;
|
Boolean disabled;
|
||||||
|
|
||||||
|
PluginInfo(Plugin p) {
|
||||||
|
try {
|
||||||
|
id = URLEncoder.encode(p.getName(), "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("Cannot encode plugin id", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
version = p.getVersion();
|
||||||
|
disabled = p.isDisabled() ? true : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,6 +104,14 @@ public class PluginLoader implements LifecycleListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Plugin get(String name) {
|
||||||
|
Plugin p = running.get(name);
|
||||||
|
if (p != null) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
return disabled.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
public Iterable<Plugin> getPlugins(boolean all) {
|
public Iterable<Plugin> getPlugins(boolean all) {
|
||||||
if (!all) {
|
if (!all) {
|
||||||
return running.values();
|
return running.values();
|
||||||
|
@ -14,10 +14,14 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.plugins;
|
package com.google.gerrit.server.plugins;
|
||||||
|
|
||||||
|
import static com.google.gerrit.server.plugins.PluginResource.PLUGIN_KIND;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestApiModule;
|
||||||
import com.google.gerrit.extensions.systemstatus.ServerInformation;
|
import com.google.gerrit.extensions.systemstatus.ServerInformation;
|
||||||
import com.google.gerrit.lifecycle.LifecycleModule;
|
import com.google.gerrit.lifecycle.LifecycleModule;
|
||||||
|
|
||||||
public class PluginModule extends LifecycleModule {
|
public class PluginModule extends RestApiModule {
|
||||||
@Override
|
@Override
|
||||||
protected void configure() {
|
protected void configure() {
|
||||||
bind(ServerInformationImpl.class);
|
bind(ServerInformationImpl.class);
|
||||||
@ -26,8 +30,20 @@ public class PluginModule extends LifecycleModule {
|
|||||||
bind(PluginCleanerTask.class);
|
bind(PluginCleanerTask.class);
|
||||||
bind(PluginGuiceEnvironment.class);
|
bind(PluginGuiceEnvironment.class);
|
||||||
bind(PluginLoader.class);
|
bind(PluginLoader.class);
|
||||||
|
|
||||||
bind(CopyConfigModule.class);
|
bind(CopyConfigModule.class);
|
||||||
listener().to(PluginLoader.class);
|
install(new LifecycleModule() {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
listener().to(PluginLoader.class);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
DynamicMap.mapOf(binder(), PLUGIN_KIND);
|
||||||
|
put(PLUGIN_KIND).to(InstallPlugin.Overwrite.class);
|
||||||
|
delete(PLUGIN_KIND).to(DisablePlugin.class);
|
||||||
|
get(PLUGIN_KIND, "status").to(GetStatus.class);
|
||||||
|
post(PLUGIN_KIND, "disable").to(DisablePlugin.class);
|
||||||
|
post(PLUGIN_KIND, "enable").to(EnablePlugin.class);
|
||||||
|
post(PLUGIN_KIND, "reload").to(ReloadPlugin.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.plugins;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.restapi.RestResource;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestView;
|
||||||
|
import com.google.inject.TypeLiteral;
|
||||||
|
|
||||||
|
public class PluginResource implements RestResource {
|
||||||
|
public static final TypeLiteral<RestView<PluginResource>> PLUGIN_KIND =
|
||||||
|
new TypeLiteral<RestView<PluginResource>>() {};
|
||||||
|
|
||||||
|
private final Plugin plugin;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
PluginResource(Plugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.name = plugin.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginResource(String name) {
|
||||||
|
this.plugin = null;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Plugin getPlugin() {
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.plugins;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||||
|
import com.google.gerrit.extensions.restapi.AcceptsCreate;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestCollection;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestView;
|
||||||
|
import com.google.gerrit.extensions.restapi.TopLevelResource;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Provider;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
|
||||||
|
public class PluginsCollection implements
|
||||||
|
RestCollection<TopLevelResource, PluginResource>,
|
||||||
|
AcceptsCreate<TopLevelResource> {
|
||||||
|
|
||||||
|
private final DynamicMap<RestView<PluginResource>> views;
|
||||||
|
private final PluginLoader loader;
|
||||||
|
private final Provider<ListPlugins> list;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
PluginsCollection(DynamicMap<RestView<PluginResource>> views,
|
||||||
|
PluginLoader loader, Provider<ListPlugins> list) {
|
||||||
|
this.views = views;
|
||||||
|
this.loader = loader;
|
||||||
|
this.list = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RestView<TopLevelResource> list() throws ResourceNotFoundException {
|
||||||
|
return list.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginResource parse(TopLevelResource parent, String id)
|
||||||
|
throws ResourceNotFoundException, Exception {
|
||||||
|
Plugin p = loader.get(decode(id));
|
||||||
|
if (p == null) {
|
||||||
|
throw new ResourceNotFoundException(id);
|
||||||
|
}
|
||||||
|
return new PluginResource(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public InstallPlugin create(TopLevelResource parent, String id)
|
||||||
|
throws ResourceNotFoundException {
|
||||||
|
return new InstallPlugin(loader, decode(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DynamicMap<RestView<PluginResource>> views() {
|
||||||
|
return views;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String decode(String id) {
|
||||||
|
try {
|
||||||
|
return URLDecoder.decode(id, "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("JVM does not support UTF-8", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.plugins;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.gerrit.common.data.GlobalCapability;
|
||||||
|
import com.google.gerrit.extensions.annotations.RequiresCapability;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||||
|
import com.google.gerrit.server.plugins.ReloadPlugin.Input;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
|
||||||
|
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
|
||||||
|
class ReloadPlugin implements RestModifyView<PluginResource, Input> {
|
||||||
|
static class Input {
|
||||||
|
}
|
||||||
|
|
||||||
|
private final PluginLoader loader;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ReloadPlugin(PluginLoader loader) {
|
||||||
|
this.loader = loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<Input> inputType() {
|
||||||
|
return Input.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object apply(PluginResource resource, Input input) throws ResourceConflictException {
|
||||||
|
String name = resource.getName();
|
||||||
|
try {
|
||||||
|
loader.reload(ImmutableList.of(name));
|
||||||
|
} catch (InvalidPluginException e) {
|
||||||
|
throw new ResourceConflictException(e.getMessage());
|
||||||
|
} catch (PluginInstallException e) {
|
||||||
|
StringWriter buf = new StringWriter();
|
||||||
|
buf.write(String.format("cannot reload %s\n", name));
|
||||||
|
PrintWriter pw = new PrintWriter(buf);
|
||||||
|
e.printStackTrace(pw);
|
||||||
|
pw.flush();
|
||||||
|
throw new ResourceConflictException(buf.toString());
|
||||||
|
}
|
||||||
|
return new ListPlugins.PluginInfo(loader.get(name));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.project;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.restapi.RestResource;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestView;
|
||||||
|
import com.google.inject.TypeLiteral;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lib.Config;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
|
|
||||||
|
public class DashboardResource implements RestResource {
|
||||||
|
public static final TypeLiteral<RestView<DashboardResource>> DASHBOARD_KIND =
|
||||||
|
new TypeLiteral<RestView<DashboardResource>>() {};
|
||||||
|
|
||||||
|
private final ProjectControl control;
|
||||||
|
private final String refName;
|
||||||
|
private final String pathName;
|
||||||
|
private final ObjectId objId;
|
||||||
|
private final Config config;
|
||||||
|
|
||||||
|
DashboardResource(ProjectControl control,
|
||||||
|
String refName,
|
||||||
|
String pathName,
|
||||||
|
ObjectId objId,
|
||||||
|
Config config) {
|
||||||
|
this.control = control;
|
||||||
|
this.refName = refName;
|
||||||
|
this.pathName = pathName;
|
||||||
|
this.objId = objId;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectControl getControl() {
|
||||||
|
return control;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRefName() {
|
||||||
|
return refName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPathName() {
|
||||||
|
return pathName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectId getObjectId() {
|
||||||
|
return objId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Config getConfig() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,182 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.project;
|
||||||
|
|
||||||
|
import static com.google.gerrit.server.git.GitRepositoryManager.REFS_DASHBOARDS;
|
||||||
|
|
||||||
|
import com.google.common.base.Joiner;
|
||||||
|
import com.google.common.base.Objects;
|
||||||
|
import com.google.common.base.Splitter;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||||
|
import com.google.gerrit.extensions.restapi.ChildCollection;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestView;
|
||||||
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
|
import com.google.gerrit.server.UrlEncoded;
|
||||||
|
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Provider;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.errors.AmbiguousObjectException;
|
||||||
|
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
|
||||||
|
import org.eclipse.jgit.errors.RepositoryNotFoundException;
|
||||||
|
import org.eclipse.jgit.lib.BlobBasedConfig;
|
||||||
|
import org.eclipse.jgit.lib.Config;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
class DashboardsCollection implements
|
||||||
|
ChildCollection<ProjectResource, DashboardResource> {
|
||||||
|
private final GitRepositoryManager gitManager;
|
||||||
|
private final DynamicMap<RestView<DashboardResource>> views;
|
||||||
|
private final Provider<ListDashboards> list;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
DashboardsCollection(GitRepositoryManager gitManager,
|
||||||
|
DynamicMap<RestView<DashboardResource>> views,
|
||||||
|
Provider<ListDashboards> list) {
|
||||||
|
this.gitManager = gitManager;
|
||||||
|
this.views = views;
|
||||||
|
this.list = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RestView<ProjectResource> list() throws ResourceNotFoundException {
|
||||||
|
return list.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DashboardResource parse(ProjectResource parent, String id)
|
||||||
|
throws ResourceNotFoundException, Exception {
|
||||||
|
ProjectControl ctl = parent.getControl();
|
||||||
|
if ("default".equals(id)) {
|
||||||
|
id = defaultOf(ctl.getProject());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> parts = Lists.newArrayList(
|
||||||
|
Splitter.on(':').limit(2).split(id));
|
||||||
|
if (parts.size() != 2) {
|
||||||
|
throw new ResourceNotFoundException(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
String ref = URLDecoder.decode(parts.get(0), "UTF-8");
|
||||||
|
String path = URLDecoder.decode(parts.get(1), "UTF-8");
|
||||||
|
if (!ref.startsWith(REFS_DASHBOARDS)) {
|
||||||
|
ref = REFS_DASHBOARDS + ref;
|
||||||
|
}
|
||||||
|
if (!Repository.isValidRefName(ref)
|
||||||
|
|| !ctl.controlForRef(ref).canRead()) {
|
||||||
|
throw new ResourceNotFoundException(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Repository git;
|
||||||
|
try {
|
||||||
|
git = gitManager.openRepository(parent.getNameKey());
|
||||||
|
} catch (RepositoryNotFoundException e) {
|
||||||
|
throw new ResourceNotFoundException(id);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ObjectId objId;
|
||||||
|
try {
|
||||||
|
objId = git.resolve(Joiner.on(':').join(ref, path));
|
||||||
|
} catch (AmbiguousObjectException e) {
|
||||||
|
throw new ResourceNotFoundException(id);
|
||||||
|
} catch (IncorrectObjectTypeException e) {
|
||||||
|
throw new ResourceNotFoundException(id);
|
||||||
|
}
|
||||||
|
if (objId == null) {
|
||||||
|
throw new ResourceNotFoundException();
|
||||||
|
}
|
||||||
|
BlobBasedConfig cfg = new BlobBasedConfig(null, git, objId);
|
||||||
|
return new DashboardResource(ctl, ref, path, objId, cfg);
|
||||||
|
} finally {
|
||||||
|
git.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DynamicMap<RestView<DashboardResource>> views() {
|
||||||
|
return views;
|
||||||
|
}
|
||||||
|
|
||||||
|
static DashboardInfo parse(Project project, String refName, String path,
|
||||||
|
Config config) throws UnsupportedEncodingException {
|
||||||
|
DashboardInfo info = new DashboardInfo(refName, path);
|
||||||
|
info.title = config.getString("dashboard", null, "title");
|
||||||
|
info.description = config.getString("dashboard", null, "description");
|
||||||
|
info.isDefault = info.id.equals(defaultOf(project)) ? true : null;
|
||||||
|
|
||||||
|
UrlEncoded u = new UrlEncoded("/dashboard/");
|
||||||
|
u.put("title", Objects.firstNonNull(info.title, info.path));
|
||||||
|
for (String name : config.getSubsections("section")) {
|
||||||
|
Section s = new Section();
|
||||||
|
s.name = name;
|
||||||
|
s.query = config.getString("section", name, "query");
|
||||||
|
u.put(s.name, replace(project.getName(), s.query));
|
||||||
|
info.sections.add(s);
|
||||||
|
}
|
||||||
|
info.url = u.toString().replace("%3A", ":");
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String replace(String project, String query) {
|
||||||
|
return query.replace("${project}", project);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String defaultOf(Project proj) {
|
||||||
|
return Objects.firstNonNull(
|
||||||
|
proj.getLocalDefaultDashboard(),
|
||||||
|
Strings.nullToEmpty(proj.getDefaultDashboard()));
|
||||||
|
}
|
||||||
|
|
||||||
|
static class DashboardInfo {
|
||||||
|
final String kind = "gerritcodereview#dashboard";
|
||||||
|
String id;
|
||||||
|
String project;
|
||||||
|
String ref;
|
||||||
|
String path;
|
||||||
|
String description;
|
||||||
|
String url;
|
||||||
|
|
||||||
|
@SerializedName("default")
|
||||||
|
Boolean isDefault;
|
||||||
|
|
||||||
|
String title;
|
||||||
|
List<Section> sections = Lists.newArrayList();
|
||||||
|
|
||||||
|
DashboardInfo(String ref, String name)
|
||||||
|
throws UnsupportedEncodingException {
|
||||||
|
this.ref = ref;
|
||||||
|
this.path = name;
|
||||||
|
this.id = Joiner.on(':').join(
|
||||||
|
URLEncoder.encode(ref,"UTF-8"),
|
||||||
|
URLEncoder.encode(path, "UTF-8"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Section {
|
||||||
|
String name;
|
||||||
|
String query;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.project;
|
||||||
|
|
||||||
|
import static com.google.gerrit.server.git.GitRepositoryManager.REFS_DASHBOARDS;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
|
||||||
|
class GetDashboard implements RestReadView<DashboardResource> {
|
||||||
|
@Override
|
||||||
|
public Object apply(DashboardResource resource)
|
||||||
|
throws UnsupportedEncodingException {
|
||||||
|
return DashboardsCollection.parse(
|
||||||
|
resource.getControl().getProject(),
|
||||||
|
resource.getRefName().substring(REFS_DASHBOARDS.length()),
|
||||||
|
resource.getPathName(),
|
||||||
|
resource.getConfig());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.project;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||||
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
|
|
||||||
|
class GetDescription implements RestReadView<ProjectResource> {
|
||||||
|
@Override
|
||||||
|
public Object apply(ProjectResource resource) {
|
||||||
|
Project project = resource.getControl().getProject();
|
||||||
|
return Strings.nullToEmpty(project.getDescription());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.project;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||||
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
|
|
||||||
|
class GetParent implements RestReadView<ProjectResource> {
|
||||||
|
@Override
|
||||||
|
public Object apply(ProjectResource resource) {
|
||||||
|
Project project = resource.getControl().getProject();
|
||||||
|
return Strings.nullToEmpty(project.getParentName());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.project;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||||
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
|
||||||
|
class GetProject implements RestReadView<ProjectResource> {
|
||||||
|
@Override
|
||||||
|
public Object apply(ProjectResource resource) throws UnsupportedEncodingException {
|
||||||
|
Project project = resource.getControl().getProject();
|
||||||
|
ProjectInfo info = new ProjectInfo();
|
||||||
|
info.name = resource.getName();
|
||||||
|
info.parent = Strings.emptyToNull(project.getParentName());
|
||||||
|
info.description = Strings.emptyToNull(project.getDescription());
|
||||||
|
info.finish();
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ProjectInfo {
|
||||||
|
final String kind = "gerritcodereview#project";
|
||||||
|
String id;
|
||||||
|
String name;
|
||||||
|
String parent;
|
||||||
|
String description;
|
||||||
|
|
||||||
|
void finish() throws UnsupportedEncodingException {
|
||||||
|
id = URLEncoder.encode(name, "UTF-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,152 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.project;
|
||||||
|
|
||||||
|
import static com.google.gerrit.server.git.GitRepositoryManager.REFS_DASHBOARDS;
|
||||||
|
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
import com.google.gerrit.extensions.restapi.AuthException;
|
||||||
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||||
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
|
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||||
|
import com.google.gerrit.server.project.DashboardsCollection.DashboardInfo;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
|
import org.eclipse.jgit.errors.RepositoryNotFoundException;
|
||||||
|
import org.eclipse.jgit.lib.BlobBasedConfig;
|
||||||
|
import org.eclipse.jgit.lib.FileMode;
|
||||||
|
import org.eclipse.jgit.lib.Ref;
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
|
import org.eclipse.jgit.treewalk.TreeWalk;
|
||||||
|
import org.kohsuke.args4j.Option;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
class ListDashboards implements RestReadView<ProjectResource> {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DashboardsCollection.class);
|
||||||
|
private final GitRepositoryManager gitManager;
|
||||||
|
private final ProjectControl.GenericFactory projectFactory;
|
||||||
|
|
||||||
|
@Option(name = "--inherited", usage = "include inherited dashboards")
|
||||||
|
private boolean inherited;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ListDashboards(GitRepositoryManager gitManager,
|
||||||
|
ProjectControl.GenericFactory projectFactory) {
|
||||||
|
this.gitManager = gitManager;
|
||||||
|
this.projectFactory = projectFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object apply(ProjectResource resource) throws AuthException,
|
||||||
|
BadRequestException, ResourceConflictException, Exception {
|
||||||
|
if (!inherited) {
|
||||||
|
return scan(resource.getControl());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<DashboardInfo>> all = Lists.newArrayList();
|
||||||
|
ProjectControl ctl = resource.getControl();
|
||||||
|
Set<Project.NameKey> seen = Sets.newHashSet();
|
||||||
|
for (;;) {
|
||||||
|
if (ctl.isVisible()) {
|
||||||
|
List<DashboardInfo> list = scan(ctl);
|
||||||
|
for (DashboardInfo d : list) {
|
||||||
|
d.project = ctl.getProject().getName();
|
||||||
|
}
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
all.add(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectState ps = ctl.getProjectState().getParentState();
|
||||||
|
if (ps == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Project.NameKey name = ps.getProject().getNameKey();
|
||||||
|
if (!seen.add(name)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctl = projectFactory.controlFor(name, ctl.getCurrentUser());
|
||||||
|
}
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DashboardInfo> scan(ProjectControl ctl) throws AuthException,
|
||||||
|
BadRequestException, ResourceConflictException, Exception {
|
||||||
|
Repository git;
|
||||||
|
try {
|
||||||
|
git = gitManager.openRepository(ctl.getProject().getNameKey());
|
||||||
|
} catch (RepositoryNotFoundException e) {
|
||||||
|
throw new ResourceNotFoundException();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
RevWalk rw = new RevWalk(git);
|
||||||
|
try {
|
||||||
|
List<DashboardInfo> all = Lists.newArrayList();
|
||||||
|
for (Ref ref : git.getRefDatabase().getRefs(REFS_DASHBOARDS).values()) {
|
||||||
|
if (ctl.controlForRef(ref.getName()).canRead()) {
|
||||||
|
all.addAll(scanDashboards(ctl.getProject(), git, rw, ref));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return all;
|
||||||
|
} finally {
|
||||||
|
rw.release();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
git.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DashboardInfo> scanDashboards(Project project,
|
||||||
|
Repository git, RevWalk rw, Ref ref) throws IOException {
|
||||||
|
List<DashboardInfo> list = Lists.newArrayList();
|
||||||
|
TreeWalk tw = new TreeWalk(rw.getObjectReader());
|
||||||
|
try {
|
||||||
|
tw.addTree(rw.parseTree(ref.getObjectId()));
|
||||||
|
tw.setRecursive(true);
|
||||||
|
while (tw.next()) {
|
||||||
|
if (tw.getFileMode(0) == FileMode.REGULAR_FILE) {
|
||||||
|
try {
|
||||||
|
list.add(DashboardsCollection.parse(
|
||||||
|
project,
|
||||||
|
ref.getName().substring(REFS_DASHBOARDS.length()),
|
||||||
|
tw.getPathString(),
|
||||||
|
new BlobBasedConfig(null, git, tw.getObjectId(0))));
|
||||||
|
} catch (ConfigInvalidException e) {
|
||||||
|
log.warn(String.format(
|
||||||
|
"Cannot parse dashboard %s:%s:%s: %s",
|
||||||
|
project.getName(), ref.getName(), tw.getPathString(),
|
||||||
|
e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
tw.release();
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
@ -14,9 +14,16 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.project;
|
package com.google.gerrit.server.project;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.google.gerrit.common.data.GroupReference;
|
import com.google.gerrit.common.data.GroupReference;
|
||||||
import com.google.gerrit.common.errors.NoSuchGroupException;
|
import com.google.gerrit.common.errors.NoSuchGroupException;
|
||||||
|
import com.google.gerrit.extensions.restapi.AuthException;
|
||||||
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
|
import com.google.gerrit.extensions.restapi.BinaryResult;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||||
|
import com.google.gerrit.extensions.restapi.TopLevelResource;
|
||||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
import com.google.gerrit.reviewdb.client.AccountGroup;
|
||||||
import com.google.gerrit.reviewdb.client.Project;
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
import com.google.gerrit.reviewdb.client.Project.NameKey;
|
import com.google.gerrit.reviewdb.client.Project.NameKey;
|
||||||
@ -27,6 +34,7 @@ import com.google.gerrit.server.account.GroupCache;
|
|||||||
import com.google.gerrit.server.account.GroupControl;
|
import com.google.gerrit.server.account.GroupControl;
|
||||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||||
import com.google.gerrit.server.util.TreeFormatter;
|
import com.google.gerrit.server.util.TreeFormatter;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
@ -39,11 +47,13 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.BufferedWriter;
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -54,7 +64,7 @@ import java.util.TreeMap;
|
|||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
|
||||||
/** List projects visible to the calling user. */
|
/** List projects visible to the calling user. */
|
||||||
public class ListProjects {
|
public class ListProjects implements RestReadView<TopLevelResource> {
|
||||||
private static final Logger log = LoggerFactory.getLogger(ListProjects.class);
|
private static final Logger log = LoggerFactory.getLogger(ListProjects.class);
|
||||||
|
|
||||||
public static enum FilterType {
|
public static enum FilterType {
|
||||||
@ -96,7 +106,8 @@ public class ListProjects {
|
|||||||
private final GitRepositoryManager repoManager;
|
private final GitRepositoryManager repoManager;
|
||||||
private final ProjectNode.Factory projectNodeFactory;
|
private final ProjectNode.Factory projectNodeFactory;
|
||||||
|
|
||||||
@Option(name = "--format", metaVar = "FMT", usage = "Output display format")
|
@Deprecated
|
||||||
|
@Option(name = "--format", usage = "(deprecated) output format")
|
||||||
private OutputFormat format = OutputFormat.TEXT;
|
private OutputFormat format = OutputFormat.TEXT;
|
||||||
|
|
||||||
@Option(name = "--show-branch", aliases = {"-b"}, multiValued = true,
|
@Option(name = "--show-branch", aliases = {"-b"}, multiValued = true,
|
||||||
@ -120,6 +131,7 @@ public class ListProjects {
|
|||||||
@Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of projects to list")
|
@Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of projects to list")
|
||||||
private int limit;
|
private int limit;
|
||||||
|
|
||||||
|
@Option(name = "-p", metaVar = "PERFIX", usage = "match project prefix")
|
||||||
private String matchPrefix;
|
private String matchPrefix;
|
||||||
|
|
||||||
@Option(name = "--has-acl-for", metaVar = "GROUP", usage =
|
@Option(name = "--has-acl-for", metaVar = "GROUP", usage =
|
||||||
@ -155,7 +167,7 @@ public class ListProjects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ListProjects setFormat(OutputFormat fmt) {
|
public ListProjects setFormat(OutputFormat fmt) {
|
||||||
this.format = fmt;
|
format = fmt;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,13 +176,29 @@ public class ListProjects {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void display(OutputStream out) {
|
@Override
|
||||||
final PrintWriter stdout;
|
public Object apply(TopLevelResource resource) throws AuthException,
|
||||||
try {
|
BadRequestException, ResourceConflictException, Exception {
|
||||||
stdout = new PrintWriter(new BufferedWriter(new OutputStreamWriter(out, "UTF-8")));
|
if (format == OutputFormat.TEXT) {
|
||||||
} catch (UnsupportedEncodingException e) {
|
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
||||||
// Our encoding is required by the specifications for the runtime.
|
display(buf);
|
||||||
throw new RuntimeException("JVM lacks UTF-8 encoding", e);
|
return BinaryResult.create(buf.toByteArray())
|
||||||
|
.setContentType("text/plain")
|
||||||
|
.setCharacterEncoding("UTF-8");
|
||||||
|
}
|
||||||
|
format = OutputFormat.JSON;
|
||||||
|
return display(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonElement display(OutputStream displayOutputStream) {
|
||||||
|
PrintWriter stdout = null;
|
||||||
|
if (displayOutputStream != null) {
|
||||||
|
try {
|
||||||
|
stdout = new PrintWriter(new BufferedWriter(
|
||||||
|
new OutputStreamWriter(displayOutputStream, "UTF-8")));
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("JVM lacks UTF-8 encoding", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int found = 0;
|
int found = 0;
|
||||||
@ -212,8 +240,9 @@ public class ListProjects {
|
|||||||
&& !rejected.contains(parentState.getProject().getName())) {
|
&& !rejected.contains(parentState.getProject().getName())) {
|
||||||
ProjectControl parentCtrl = parentState.controlFor(currentUser);
|
ProjectControl parentCtrl = parentState.controlFor(currentUser);
|
||||||
if (parentCtrl.isVisible() || parentCtrl.isOwner()) {
|
if (parentCtrl.isVisible() || parentCtrl.isOwner()) {
|
||||||
info.name = parentState.getProject().getName();
|
info.setName(parentState.getProject().getName());
|
||||||
info.description = parentState.getProject().getDescription();
|
info.description = Strings.emptyToNull(
|
||||||
|
parentState.getProject().getDescription());
|
||||||
} else {
|
} else {
|
||||||
rejected.add(parentState.getProject().getName());
|
rejected.add(parentState.getProject().getName());
|
||||||
continue;
|
continue;
|
||||||
@ -236,7 +265,7 @@ public class ListProjects {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
info.name = projectName.get();
|
info.setName(projectName.get());
|
||||||
if (showTree && format.isJson()) {
|
if (showTree && format.isJson()) {
|
||||||
ProjectState parent = e.getParentState();
|
ProjectState parent = e.getParentState();
|
||||||
if (parent != null) {
|
if (parent != null) {
|
||||||
@ -252,8 +281,8 @@ public class ListProjects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (showDescription && !e.getProject().getDescription().isEmpty()) {
|
if (showDescription) {
|
||||||
info.description = e.getProject().getDescription();
|
info.description = Strings.emptyToNull(e.getProject().getDescription());
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -305,7 +334,7 @@ public class ListProjects {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format.isJson()) {
|
if (stdout == null || format.isJson()) {
|
||||||
output.put(info.name, info);
|
output.put(info.name, info);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -330,15 +359,22 @@ public class ListProjects {
|
|||||||
stdout.print('\n');
|
stdout.print('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format.isJson()) {
|
if (stdout == null) {
|
||||||
|
return OutputFormat.JSON.newGson().toJsonTree(
|
||||||
|
output,
|
||||||
|
new TypeToken<Map<String, Object>>() {}.getType());
|
||||||
|
} else if (format.isJson()) {
|
||||||
format.newGson().toJson(
|
format.newGson().toJson(
|
||||||
output, new TypeToken<Map<String, ProjectInfo>>() {}.getType(), stdout);
|
output, new TypeToken<Map<String, ProjectInfo>>() {}.getType(), stdout);
|
||||||
stdout.print('\n');
|
stdout.print('\n');
|
||||||
} else if (showTree && treeMap.size() > 0) {
|
} else if (showTree && treeMap.size() > 0) {
|
||||||
printProjectTree(stdout, treeMap);
|
printProjectTree(stdout, treeMap);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
stdout.flush();
|
if (stdout != null) {
|
||||||
|
stdout.flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,12 +444,23 @@ public class ListProjects {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ProjectInfo {
|
static class ProjectInfo {
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
final String kind = "gerritcodereview#project";
|
final String kind = "gerritcodereview#project";
|
||||||
|
|
||||||
transient String name;
|
transient String name;
|
||||||
|
String id;
|
||||||
String parent;
|
String parent;
|
||||||
String description;
|
String description;
|
||||||
Map<String, String> branches;
|
Map<String, String> branches;
|
||||||
|
|
||||||
|
void setName(String name) {
|
||||||
|
try {
|
||||||
|
this.name = name;
|
||||||
|
id = URLEncoder.encode(name, "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
log.warn("Cannot UTF-8 encode project id", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.project;
|
||||||
|
|
||||||
|
import static com.google.gerrit.server.project.DashboardResource.DASHBOARD_KIND;
|
||||||
|
import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestApiModule;
|
||||||
|
|
||||||
|
public class Module extends RestApiModule {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
DynamicMap.mapOf(binder(), PROJECT_KIND);
|
||||||
|
DynamicMap.mapOf(binder(), DASHBOARD_KIND);
|
||||||
|
|
||||||
|
get(PROJECT_KIND).to(GetProject.class);
|
||||||
|
get(PROJECT_KIND, "description").to(GetDescription.class);
|
||||||
|
put(PROJECT_KIND, "description").to(SetDescription.class);
|
||||||
|
delete(PROJECT_KIND, "description").to(SetDescription.class);
|
||||||
|
|
||||||
|
get(PROJECT_KIND, "parent").to(GetParent.class);
|
||||||
|
put(PROJECT_KIND, "parent").to(SetParent.class);
|
||||||
|
|
||||||
|
child(PROJECT_KIND, "dashboards").to(DashboardsCollection.class);
|
||||||
|
get(DASHBOARD_KIND).to(GetDashboard.class);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.project;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.restapi.RestResource;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestView;
|
||||||
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
|
import com.google.inject.TypeLiteral;
|
||||||
|
|
||||||
|
public class ProjectResource implements RestResource {
|
||||||
|
public static final TypeLiteral<RestView<ProjectResource>> PROJECT_KIND =
|
||||||
|
new TypeLiteral<RestView<ProjectResource>>() {};
|
||||||
|
|
||||||
|
private final ProjectControl control;
|
||||||
|
|
||||||
|
ProjectResource(ProjectControl control) {
|
||||||
|
this.control = control;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return control.getProject().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Project.NameKey getNameKey() {
|
||||||
|
return control.getProject().getNameKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProjectControl getControl() {
|
||||||
|
return control;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.project;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestCollection;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestView;
|
||||||
|
import com.google.gerrit.extensions.restapi.TopLevelResource;
|
||||||
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
|
import com.google.gerrit.server.CurrentUser;
|
||||||
|
import com.google.gerrit.server.OutputFormat;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Provider;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
|
||||||
|
public class ProjectsCollection implements
|
||||||
|
RestCollection<TopLevelResource, ProjectResource> {
|
||||||
|
private final DynamicMap<RestView<ProjectResource>> views;
|
||||||
|
private final Provider<ListProjects> list;
|
||||||
|
private final ProjectControl.GenericFactory controlFactory;
|
||||||
|
private final Provider<CurrentUser> user;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ProjectsCollection(DynamicMap<RestView<ProjectResource>> views,
|
||||||
|
Provider<ListProjects> list,
|
||||||
|
ProjectControl.GenericFactory controlFactory,
|
||||||
|
Provider<CurrentUser> user) {
|
||||||
|
this.views = views;
|
||||||
|
this.list = list;
|
||||||
|
this.controlFactory = controlFactory;
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RestView<TopLevelResource> list() {
|
||||||
|
return list.get().setFormat(OutputFormat.JSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProjectResource parse(TopLevelResource parent, String id)
|
||||||
|
throws ResourceNotFoundException {
|
||||||
|
ProjectControl ctl;
|
||||||
|
try {
|
||||||
|
ctl = controlFactory.controlFor(decode(id), user.get());
|
||||||
|
} catch (NoSuchProjectException e) {
|
||||||
|
throw new ResourceNotFoundException(id);
|
||||||
|
}
|
||||||
|
if (!ctl.isVisible() && !ctl.isOwner()) {
|
||||||
|
throw new ResourceNotFoundException(id);
|
||||||
|
}
|
||||||
|
return new ProjectResource(ctl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Project.NameKey decode(String id) {
|
||||||
|
try {
|
||||||
|
return new Project.NameKey(URLDecoder.decode(id, "UTF-8"));
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("JVM does not support UTF-8", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DynamicMap<RestView<ProjectResource>> views() {
|
||||||
|
return views;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.project;
|
||||||
|
|
||||||
|
import com.google.common.base.Objects;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.gerrit.extensions.restapi.AuthException;
|
||||||
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
|
import com.google.gerrit.extensions.restapi.DefaultInput;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||||
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
|
import com.google.gerrit.server.git.MetaDataUpdate;
|
||||||
|
import com.google.gerrit.server.git.ProjectConfig;
|
||||||
|
import com.google.gerrit.server.project.SetDescription.Input;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
|
import org.eclipse.jgit.errors.RepositoryNotFoundException;
|
||||||
|
|
||||||
|
class SetDescription implements RestModifyView<ProjectResource, Input> {
|
||||||
|
static class Input {
|
||||||
|
@DefaultInput
|
||||||
|
String description;
|
||||||
|
String commitMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ProjectCache cache;
|
||||||
|
private final MetaDataUpdate.Server updateFactory;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SetDescription(ProjectCache cache, MetaDataUpdate.Server updateFactory) {
|
||||||
|
this.cache = cache;
|
||||||
|
this.updateFactory = updateFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<Input> inputType() {
|
||||||
|
return Input.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object apply(ProjectResource resource, Input input)
|
||||||
|
throws AuthException, BadRequestException, ResourceConflictException,
|
||||||
|
Exception {
|
||||||
|
if (input == null) {
|
||||||
|
input = new Input(); // Delete would set description to null.
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectControl ctl = resource.getControl();
|
||||||
|
IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
|
||||||
|
if (!ctl.isOwner()) {
|
||||||
|
throw new AuthException("not project owner");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
MetaDataUpdate md = updateFactory.create(resource.getNameKey());
|
||||||
|
try {
|
||||||
|
ProjectConfig config = ProjectConfig.read(md);
|
||||||
|
Project project = config.getProject();
|
||||||
|
project.setDescription(Strings.emptyToNull(input.description));
|
||||||
|
|
||||||
|
String msg = Objects.firstNonNull(
|
||||||
|
Strings.emptyToNull(input.commitMessage),
|
||||||
|
"Updated description.\n");
|
||||||
|
if (!msg.endsWith("\n")) {
|
||||||
|
msg += "\n";
|
||||||
|
}
|
||||||
|
md.setAuthor(user);
|
||||||
|
md.setMessage(msg);
|
||||||
|
config.commit(md);
|
||||||
|
cache.evict(ctl.getProject());
|
||||||
|
|
||||||
|
ListProjects.ProjectInfo info = new ListProjects.ProjectInfo();
|
||||||
|
info.setName(resource.getName());
|
||||||
|
info.parent = project.getParentName();
|
||||||
|
info.description = project.getDescription();
|
||||||
|
return info;
|
||||||
|
} finally {
|
||||||
|
md.close();
|
||||||
|
}
|
||||||
|
} catch (RepositoryNotFoundException notFound) {
|
||||||
|
throw new ResourceNotFoundException(resource.getName());
|
||||||
|
} catch (ConfigInvalidException e) {
|
||||||
|
throw new ResourceConflictException(String.format(
|
||||||
|
"invalid project.config: %s", e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,106 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.project;
|
||||||
|
|
||||||
|
import com.google.common.base.Objects;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.gerrit.extensions.restapi.AuthException;
|
||||||
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
|
import com.google.gerrit.extensions.restapi.DefaultInput;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||||
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
|
import com.google.gerrit.server.config.AllProjectsName;
|
||||||
|
import com.google.gerrit.server.git.MetaDataUpdate;
|
||||||
|
import com.google.gerrit.server.git.ProjectConfig;
|
||||||
|
import com.google.gerrit.server.project.SetParent.Input;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
|
import org.eclipse.jgit.errors.RepositoryNotFoundException;
|
||||||
|
|
||||||
|
class SetParent implements RestModifyView<ProjectResource, Input> {
|
||||||
|
static class Input {
|
||||||
|
@DefaultInput
|
||||||
|
String parent;
|
||||||
|
String commitMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ProjectCache cache;
|
||||||
|
private final MetaDataUpdate.Server updateFactory;
|
||||||
|
private final AllProjectsName allProjects;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SetParent(ProjectCache cache,
|
||||||
|
MetaDataUpdate.Server updateFactory,
|
||||||
|
AllProjectsName allProjects) {
|
||||||
|
this.cache = cache;
|
||||||
|
this.updateFactory = updateFactory;
|
||||||
|
this.allProjects = allProjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<Input> inputType() {
|
||||||
|
return Input.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object apply(ProjectResource resource, Input input)
|
||||||
|
throws AuthException, BadRequestException, ResourceConflictException,
|
||||||
|
Exception {
|
||||||
|
ProjectControl ctl = resource.getControl();
|
||||||
|
IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
|
||||||
|
if (!user.getCapabilities().canAdministrateServer()) {
|
||||||
|
throw new AuthException("not administrator");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
MetaDataUpdate md = updateFactory.create(resource.getNameKey());
|
||||||
|
try {
|
||||||
|
ProjectConfig config = ProjectConfig.read(md);
|
||||||
|
Project project = config.getProject();
|
||||||
|
project.setParentName(Strings.emptyToNull(input.parent));
|
||||||
|
|
||||||
|
String msg = Strings.emptyToNull(input.commitMessage);
|
||||||
|
if (msg == null) {
|
||||||
|
msg = String.format(
|
||||||
|
"Changed parent to %s.\n",
|
||||||
|
Objects.firstNonNull(project.getParentName(), allProjects.get()));
|
||||||
|
} else if (!msg.endsWith("\n")) {
|
||||||
|
msg += "\n";
|
||||||
|
}
|
||||||
|
md.setAuthor(user);
|
||||||
|
md.setMessage(msg);
|
||||||
|
config.commit(md);
|
||||||
|
cache.evict(ctl.getProject());
|
||||||
|
|
||||||
|
ListProjects.ProjectInfo info = new ListProjects.ProjectInfo();
|
||||||
|
info.setName(resource.getName());
|
||||||
|
info.parent = project.getParentName();
|
||||||
|
info.description = project.getDescription();
|
||||||
|
return info;
|
||||||
|
} finally {
|
||||||
|
md.close();
|
||||||
|
}
|
||||||
|
} catch (RepositoryNotFoundException notFound) {
|
||||||
|
throw new ResourceNotFoundException(resource.getName());
|
||||||
|
} catch (ConfigInvalidException e) {
|
||||||
|
throw new ResourceConflictException(String.format(
|
||||||
|
"invalid project.config: %s", e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -127,6 +127,12 @@ public class ChangeData {
|
|||||||
change = c;
|
change = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ChangeData(final ChangeControl c) {
|
||||||
|
legacyId = c.getChange().getId();
|
||||||
|
change = c.getChange();
|
||||||
|
changeControl = c;
|
||||||
|
}
|
||||||
|
|
||||||
public void setCurrentFilePaths(String[] filePaths) {
|
public void setCurrentFilePaths(String[] filePaths) {
|
||||||
currentFiles = filePaths;
|
currentFiles = filePaths;
|
||||||
}
|
}
|
||||||
@ -191,7 +197,7 @@ public class ChangeData {
|
|||||||
return visibleTo == user;
|
return visibleTo == user;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangeControl changeControl() {
|
public ChangeControl changeControl() {
|
||||||
return changeControl;
|
return changeControl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,190 @@
|
|||||||
|
// Copyright (C) 2012 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.query.change;
|
||||||
|
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.gerrit.common.changes.ListChangesOption;
|
||||||
|
import com.google.gerrit.extensions.restapi.BinaryResult;
|
||||||
|
import com.google.gerrit.extensions.restapi.AuthException;
|
||||||
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
|
import com.google.gerrit.extensions.restapi.TopLevelResource;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||||
|
import com.google.gerrit.reviewdb.client.Change;
|
||||||
|
import com.google.gerrit.server.OutputFormat;
|
||||||
|
import com.google.gerrit.server.change.ChangeJson;
|
||||||
|
import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
|
||||||
|
import com.google.gerrit.server.project.ChangeControl;
|
||||||
|
import com.google.gerrit.server.query.QueryParseException;
|
||||||
|
import com.google.gerrit.server.ssh.SshInfo;
|
||||||
|
import com.google.gwtorm.server.OrmException;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
import org.kohsuke.args4j.Option;
|
||||||
|
|
||||||
|
import java.util.BitSet;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class QueryChanges implements RestReadView<TopLevelResource> {
|
||||||
|
private final ChangeJson json;
|
||||||
|
private final QueryProcessor imp;
|
||||||
|
private boolean reverse;
|
||||||
|
private EnumSet<ListChangesOption> options;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
@Option(name = "--format", usage = "(deprecated) output format")
|
||||||
|
private OutputFormat format;
|
||||||
|
|
||||||
|
@Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", multiValued = true, usage = "Query string")
|
||||||
|
private List<String> queries;
|
||||||
|
|
||||||
|
@Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "Maximum number of results to return")
|
||||||
|
void setLimit(int limit) {
|
||||||
|
imp.setLimit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Option(name = "-o", multiValued = true, usage = "Output options per change")
|
||||||
|
public void addOption(ListChangesOption o) {
|
||||||
|
options.add(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Option(name = "-O", usage = "Output option flags, in hex")
|
||||||
|
void setOptionFlagsHex(String hex) {
|
||||||
|
options.addAll(ListChangesOption.fromBits(Integer.parseInt(hex, 16)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Option(name = "-P", metaVar = "SORTKEY", usage = "Previous changes before SORTKEY")
|
||||||
|
void setSortKeyAfter(String key) {
|
||||||
|
// Querying for the prior page of changes requires sortkey_after predicate.
|
||||||
|
// Changes are shown most recent->least recent. The previous page of
|
||||||
|
// results contains changes that were updated after the given key.
|
||||||
|
imp.setSortkeyAfter(key);
|
||||||
|
reverse = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Option(name = "-N", metaVar = "SORTKEY", usage = "Next changes after SORTKEY")
|
||||||
|
void setSortKeyBefore(String key) {
|
||||||
|
// Querying for the next page of changes requires sortkey_before predicate.
|
||||||
|
// Changes are shown most recent->least recent. The next page contains
|
||||||
|
// changes that were updated before the given key.
|
||||||
|
imp.setSortkeyBefore(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
QueryChanges(ChangeJson json,
|
||||||
|
QueryProcessor qp,
|
||||||
|
SshInfo sshInfo,
|
||||||
|
ChangeControl.Factory cf) {
|
||||||
|
this.json = json;
|
||||||
|
this.imp = qp;
|
||||||
|
|
||||||
|
options = EnumSet.noneOf(ListChangesOption.class);
|
||||||
|
json.setSshInfo(sshInfo);
|
||||||
|
json.setChangeControlFactory(cf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object apply(TopLevelResource rsrc)
|
||||||
|
throws BadRequestException, AuthException, OrmException {
|
||||||
|
List<List<ChangeInfo>> out;
|
||||||
|
try {
|
||||||
|
out = query();
|
||||||
|
} catch (QueryParseException e) {
|
||||||
|
// This is a hack to detect an operator that requires authentication.
|
||||||
|
Pattern p = Pattern.compile("^Error in operator (.*:self)$");
|
||||||
|
Matcher m = p.matcher(e.getMessage());
|
||||||
|
if (m.matches()) {
|
||||||
|
String op = m.group(1);
|
||||||
|
throw new AuthException("Must be signed-in to use " + op);
|
||||||
|
}
|
||||||
|
throw new BadRequestException(e.getMessage());
|
||||||
|
}
|
||||||
|
if (format == OutputFormat.TEXT) {
|
||||||
|
return formatText(out);
|
||||||
|
}
|
||||||
|
return out.size() == 1 ? out.get(0) : out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BinaryResult formatText(List<List<ChangeInfo>> res) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
boolean firstQuery = true;
|
||||||
|
for (List<ChangeInfo> info : res) {
|
||||||
|
if (firstQuery) {
|
||||||
|
firstQuery = false;
|
||||||
|
} else {
|
||||||
|
sb.append('\n');
|
||||||
|
}
|
||||||
|
for (ChangeInfo c : info) {
|
||||||
|
String id = new Change.Key(c.changeId).abbreviate();
|
||||||
|
String subject = c.subject;
|
||||||
|
if (subject.length() + id.length() > 80) {
|
||||||
|
subject = subject.substring(0, 80 - id.length());
|
||||||
|
}
|
||||||
|
sb.append(id);
|
||||||
|
sb.append(' ');
|
||||||
|
sb.append(subject.replace('\n', ' '));
|
||||||
|
sb.append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return BinaryResult.create(sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<List<ChangeInfo>> query()
|
||||||
|
throws OrmException, QueryParseException {
|
||||||
|
if (imp.isDisabled()) {
|
||||||
|
throw new QueryParseException("query disabled");
|
||||||
|
}
|
||||||
|
if (queries == null || queries.isEmpty()) {
|
||||||
|
queries = Collections.singletonList("status:open");
|
||||||
|
} else if (queries.size() > 10) {
|
||||||
|
// Hard-code a default maximum number of queries to prevent
|
||||||
|
// users from submitting too much to the server in a single call.
|
||||||
|
throw new QueryParseException("limit of 10 queries");
|
||||||
|
}
|
||||||
|
|
||||||
|
int cnt = queries.size();
|
||||||
|
BitSet more = new BitSet(cnt);
|
||||||
|
List<List<ChangeData>> data = Lists.newArrayListWithCapacity(cnt);
|
||||||
|
for (int n = 0; n < cnt; n++) {
|
||||||
|
String query = queries.get(n);
|
||||||
|
List<ChangeData> changes = imp.queryChanges(query);
|
||||||
|
if (imp.getLimit() > 0 && changes.size() > imp.getLimit()) {
|
||||||
|
if (reverse) {
|
||||||
|
changes = changes.subList(1, changes.size());
|
||||||
|
} else {
|
||||||
|
changes = changes.subList(0, imp.getLimit());
|
||||||
|
}
|
||||||
|
more.set(n, true);
|
||||||
|
}
|
||||||
|
data.add(changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<ChangeInfo>> res = json.addOptions(options).formatList2(data);
|
||||||
|
for (int n = 0; n < cnt; n++) {
|
||||||
|
List<ChangeInfo> info = res.get(n);
|
||||||
|
if (more.get(n) && !info.isEmpty()) {
|
||||||
|
if (reverse) {
|
||||||
|
info.get(0)._moreChanges = true;
|
||||||
|
} else {
|
||||||
|
info.get(info.size() - 1)._moreChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
@ -38,6 +38,11 @@ limitations under the License.
|
|||||||
<artifactId>args4j</artifactId>
|
<artifactId>args4j</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.guava</groupId>
|
||||||
|
<artifactId>guava</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.inject</groupId>
|
<groupId>com.google.inject</groupId>
|
||||||
<artifactId>guice</artifactId>
|
<artifactId>guice</artifactId>
|
||||||
|
@ -34,6 +34,9 @@
|
|||||||
|
|
||||||
package com.google.gerrit.util.cli;
|
package com.google.gerrit.util.cli;
|
||||||
|
|
||||||
|
import com.google.common.collect.LinkedHashMultimap;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.collect.Multimap;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Injector;
|
import com.google.inject.Injector;
|
||||||
import com.google.inject.Key;
|
import com.google.inject.Key;
|
||||||
@ -54,11 +57,9 @@ import java.io.StringWriter;
|
|||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extended command line parser which handles --foo=value arguments.
|
* Extended command line parser which handles --foo=value arguments.
|
||||||
@ -186,7 +187,7 @@ public class CmdLineParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void parseArgument(final String... args) throws CmdLineException {
|
public void parseArgument(final String... args) throws CmdLineException {
|
||||||
final ArrayList<String> tmp = new ArrayList<String>(args.length);
|
List<String> tmp = Lists.newArrayListWithCapacity(args.length);
|
||||||
for (int argi = 0; argi < args.length; argi++) {
|
for (int argi = 0; argi < args.length; argi++) {
|
||||||
final String str = args[argi];
|
final String str = args[argi];
|
||||||
if (str.equals("--")) {
|
if (str.equals("--")) {
|
||||||
@ -211,15 +212,20 @@ public class CmdLineParser {
|
|||||||
|
|
||||||
public void parseOptionMap(Map<String, String[]> parameters)
|
public void parseOptionMap(Map<String, String[]> parameters)
|
||||||
throws CmdLineException {
|
throws CmdLineException {
|
||||||
parseOptionMap(parameters, Collections.<String>emptySet());
|
Multimap<String, String> map = LinkedHashMultimap.create();
|
||||||
|
for (Map.Entry<String, String[]> ent : parameters.entrySet()) {
|
||||||
|
for (String val : ent.getValue()) {
|
||||||
|
map.put(ent.getKey(), val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parseOptionMap(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void parseOptionMap(Map<String, String[]> parameters,
|
public void parseOptionMap(Multimap<String, String> params)
|
||||||
Set<String> argNames)
|
|
||||||
throws CmdLineException {
|
throws CmdLineException {
|
||||||
ArrayList<String> tmp = new ArrayList<String>();
|
List<String> tmp = Lists.newArrayListWithCapacity(2 * params.size());
|
||||||
for (Map.Entry<String, String[]> ent : parameters.entrySet()) {
|
for (final String key : params.keySet()) {
|
||||||
String name = ent.getKey();
|
String name = key;
|
||||||
if (!name.startsWith("-")) {
|
if (!name.startsWith("-")) {
|
||||||
if (name.length() == 1) {
|
if (name.length() == 1) {
|
||||||
name = "-" + name;
|
name = "-" + name;
|
||||||
@ -230,17 +236,15 @@ public class CmdLineParser {
|
|||||||
|
|
||||||
if (findHandler(name) instanceof BooleanOptionHandler) {
|
if (findHandler(name) instanceof BooleanOptionHandler) {
|
||||||
boolean on = false;
|
boolean on = false;
|
||||||
for (String value : ent.getValue()) {
|
for (String value : params.get(key)) {
|
||||||
on = toBoolean(ent.getKey(), value);
|
on = toBoolean(key, value);
|
||||||
}
|
}
|
||||||
if (on) {
|
if (on) {
|
||||||
tmp.add(name);
|
tmp.add(name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (String value : ent.getValue()) {
|
for (String value : params.get(key)) {
|
||||||
if (!argNames.contains(ent.getKey())) {
|
tmp.add(name);
|
||||||
tmp.add(name);
|
|
||||||
}
|
|
||||||
tmp.add(value);
|
tmp.add(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -328,7 +332,7 @@ public class CmdLineParser {
|
|||||||
private void ensureOptionsInitialized() {
|
private void ensureOptionsInitialized() {
|
||||||
if (options == null) {
|
if (options == null) {
|
||||||
help = new HelpOption();
|
help = new HelpOption();
|
||||||
options = new ArrayList<OptionHandler>();
|
options = Lists.newArrayList();
|
||||||
addOption(help, help);
|
addOption(help, help);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user