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 Format
|
||||
~~~~~~~~~~~~~
|
||||
Most APIs return text format by default. JSON can be requested
|
||||
by setting the `Accept` HTTP request header to include
|
||||
Most APIs return pretty printed JSON by default. Compact JSON can be
|
||||
requested by setting the `Accept` HTTP request header to include
|
||||
`application/json`, for example:
|
||||
|
||||
----
|
||||
@ -43,12 +43,11 @@ body to a JSON parser:
|
||||
[ ... valid JSON ... ]
|
||||
----
|
||||
|
||||
The default JSON format is `JSON_COMPACT`, which skips unnecessary
|
||||
whitespace. This is not the easiest format for a human to read. Many
|
||||
examples in this documentation use `format=JSON` as a query parameter
|
||||
to obtain pretty formatting in the response. Producing (and parsing)
|
||||
the compact format is more efficient, so most tools should prefer the
|
||||
default compact format.
|
||||
The default JSON format is pretty, which uses extra whitespace to make
|
||||
the output more readable for a human. Producing (and parsing) the
|
||||
non-pretty compact format is more efficient so tools should request it
|
||||
by using the `Accept: application/json` header or `pp=0` query
|
||||
parameter whenever possible.
|
||||
|
||||
Responses will be gzip compressed by the server if the HTTP
|
||||
`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.
|
||||
|
||||
----
|
||||
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:
|
||||
----
|
||||
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="...
|
||||
|
||||
)]}'
|
||||
@ -106,7 +105,7 @@ Filtering may decrease the response time by avoiding looking at every
|
||||
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="...
|
||||
|
||||
)]}'
|
||||
@ -128,7 +127,7 @@ using the link:cmd-ls-projects.html[ls-projects] command over SSH,
|
||||
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
|
||||
Content-Disposition: attachment
|
||||
@ -138,36 +137,112 @@ and accepts the same options as query parameters.
|
||||
{
|
||||
"external/bison": {
|
||||
"kind": "gerritcodereview#project",
|
||||
"id": "external%2Fbison",
|
||||
"description": "GNU parser generator"
|
||||
},
|
||||
"external/gcc": {},
|
||||
"external/gcc": {
|
||||
"kind": "gerritcodereview#project",
|
||||
"id": "external%2Fgcc",
|
||||
},
|
||||
"external/openssl": {
|
||||
"kind": "gerritcodereview#project",
|
||||
"id": "external%2Fopenssl",
|
||||
"description": "encryption\ncrypto routines"
|
||||
},
|
||||
"test": {
|
||||
"kind": "gerritcodereview#project",
|
||||
"id": "test",
|
||||
"description": "\u003chtml\u003e is escaped"
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[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
|
||||
prefix.
|
||||
List all projects that start with `platform/`:
|
||||
----
|
||||
GET /projects/platform/?format=JSON HTTP/1.0
|
||||
HTTP/1.1 200 OK
|
||||
Content-Disposition: attachment
|
||||
Content-Type: application/json;charset=UTF-8
|
||||
)]}'
|
||||
{
|
||||
"platform/drivers": {},
|
||||
"platform/tools": {}
|
||||
}
|
||||
GET /projects/?p=platform%2F HTTP/1.0
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Disposition: attachment
|
||||
Content-Type: application/json;charset=UTF-8
|
||||
|
||||
)]}'
|
||||
{
|
||||
"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.
|
||||
|
||||
/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/ (Query Changes)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -177,7 +252,7 @@ the returned results.
|
||||
|
||||
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
|
||||
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:
|
||||
----
|
||||
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
|
||||
Content-Disposition: attachment
|
||||
@ -304,7 +379,7 @@ default. Optional fields are:
|
||||
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
|
||||
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
|
||||
------
|
||||
|
@ -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;
|
||||
|
||||
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.rpc.NativeList;
|
||||
import com.google.gerrit.client.rpc.ScreenLoadCallback;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
|
||||
import com.google.gwt.user.client.ui.FlowPanel;
|
||||
|
||||
public class ProjectDashboardsScreen extends ProjectScreen {
|
||||
@ -33,10 +33,10 @@ public class ProjectDashboardsScreen extends ProjectScreen {
|
||||
@Override
|
||||
protected void onLoad() {
|
||||
super.onLoad();
|
||||
DashboardMap.allOnProject(getProjectKey(),
|
||||
new ScreenLoadCallback<DashboardMap>(this) {
|
||||
DashboardList.all(getProjectKey(),
|
||||
new ScreenLoadCallback<NativeList<DashboardList>>(this) {
|
||||
@Override
|
||||
protected void preDisplay(final DashboardMap result) {
|
||||
protected void preDisplay(NativeList<DashboardList> result) {
|
||||
dashes.display(result);
|
||||
}
|
||||
});
|
||||
|
@ -18,13 +18,12 @@ import com.google.gwt.core.client.JavaScriptObject;
|
||||
|
||||
public class DashboardInfo extends JavaScriptObject {
|
||||
public final native String id() /*-{ return this.id; }-*/;
|
||||
public final native String name() /*-{ return this.dashboard_name; }-*/;
|
||||
public final native String section() /*-{ return this.section; }-*/;
|
||||
public final native String refName() /*-{ return this.ref_name; }-*/;
|
||||
public final native String projectName() /*-{ return this.project_name; }-*/;
|
||||
public final native String project() /*-{ return this.project; }-*/;
|
||||
public final native String ref() /*-{ return this.ref; }-*/;
|
||||
public final native String path() /*-{ return this.path; }-*/;
|
||||
public final native String description() /*-{ return this.description; }-*/;
|
||||
public final native String parameters() /*-{ return this.parameters; }-*/;
|
||||
public final native boolean isDefault() /*-{ return this.is_default ? true : false; }-*/;
|
||||
public final native String url() /*-{ return this.url; }-*/;
|
||||
public final native boolean isDefault() /*-{ return this['default'] ? true : false; }-*/;
|
||||
|
||||
protected DashboardInfo() {
|
||||
}
|
||||
|
@ -14,27 +14,31 @@
|
||||
|
||||
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.reviewdb.client.Project;
|
||||
import com.google.gwtjsonrpc.common.AsyncCallback;
|
||||
import com.google.gwt.http.client.URL;
|
||||
import com.google.gwtjsonrpc.common.AsyncCallback;
|
||||
|
||||
/** Dashboards available from {@code /dashboards/}. */
|
||||
public class DashboardMap extends NativeMap<DashboardInfo> {
|
||||
public static void allOnProject(Project.NameKey project,
|
||||
AsyncCallback<DashboardMap> callback) {
|
||||
new RestApi("/dashboards/project/" + URL.encode(project.get()).replaceAll("[?]", "%3F"))
|
||||
.get(NativeMap.copyKeysIntoChildren(callback));
|
||||
/** Project dashboards from {@code /projects/<name>/dashboards/}. */
|
||||
public class DashboardList extends NativeList<DashboardInfo> {
|
||||
public static void all(Project.NameKey project,
|
||||
AsyncCallback<NativeList<DashboardList>> callback) {
|
||||
new RestApi(base(project))
|
||||
.addParameterTrue("inherited")
|
||||
.get(callback);
|
||||
}
|
||||
|
||||
public static void projectDefault(Project.NameKey project,
|
||||
AsyncCallback<DashboardMap> callback) {
|
||||
new RestApi("/dashboards/project/" + URL.encode(project.get()).replaceAll("[?]", "%3F"))
|
||||
.addParameterTrue("default")
|
||||
.get(NativeMap.copyKeysIntoChildren(callback));
|
||||
public static void defaultDashboard(Project.NameKey project,
|
||||
AsyncCallback<DashboardInfo> callback) {
|
||||
new RestApi(base(project) + "default").get(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;
|
||||
|
||||
import com.google.gerrit.client.Gerrit;
|
||||
import com.google.gerrit.client.rpc.NativeList;
|
||||
import com.google.gerrit.client.ui.NavigationTable;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
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.Image;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class DashboardsTable extends NavigationTable<DashboardInfo> {
|
||||
Project.NameKey project;
|
||||
@ -47,12 +51,27 @@ public class DashboardsTable extends NavigationTable<DashboardInfo> {
|
||||
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()) {
|
||||
table.removeRow(table.getRowCount() - 1);
|
||||
}
|
||||
|
||||
List<DashboardInfo> list = dashes.values().asList();
|
||||
Collections.sort(list, new Comparator<DashboardInfo>() {
|
||||
@Override
|
||||
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) {
|
||||
if (!d.section().equals(section)) {
|
||||
section = d.section();
|
||||
insertTitleRow(table.getRowCount(), section);
|
||||
if (!d.ref().equals(ref)) {
|
||||
ref = d.ref();
|
||||
insertTitleRow(table.getRowCount(), ref);
|
||||
}
|
||||
insert(table.getRowCount(), d);
|
||||
}
|
||||
@ -102,18 +121,17 @@ public class DashboardsTable extends NavigationTable<DashboardInfo> {
|
||||
final FlexCellFormatter fmt = table.getFlexCellFormatter();
|
||||
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());
|
||||
if (!project.get().equals(k.projectName())) {
|
||||
table.setText(row, 4, k.projectName());
|
||||
if (k.project() != null && !project.get().equals(k.project())) {
|
||||
table.setText(row, 4, k.project());
|
||||
}
|
||||
|
||||
setRowItem(row, k);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object getRowItemKey(final DashboardInfo item) {
|
||||
return item.name();
|
||||
return item.id();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -121,10 +139,6 @@ public class DashboardsTable extends NavigationTable<DashboardInfo> {
|
||||
if (row > 0) {
|
||||
movePointerTo(row);
|
||||
}
|
||||
History.newItem(link(getRowItem(row)));
|
||||
}
|
||||
|
||||
private String link(final DashboardInfo item) {
|
||||
return "/dashboard/?" + item.parameters();
|
||||
History.newItem(getRowItem(row).url());
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ package com.google.gerrit.client.projects;
|
||||
import com.google.gerrit.client.rpc.NativeMap;
|
||||
import com.google.gerrit.client.rpc.RestApi;
|
||||
import com.google.gwtjsonrpc.common.AsyncCallback;
|
||||
import com.google.gwt.http.client.URL;
|
||||
|
||||
/** Projects available from {@code /projects/}. */
|
||||
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) {
|
||||
new RestApi("/projects/" + URL.encode(prefix).replaceAll("[?]", "%3F"))
|
||||
.addParameterRaw("type", "ALL")
|
||||
new RestApi("/projects/")
|
||||
.addParameter("p", prefix)
|
||||
.addParameter("n", limit)
|
||||
.addParameterRaw("type", "ALL")
|
||||
.addParameterTrue("d") // description
|
||||
.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.StaticServlet;
|
||||
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.ListChangesServlet;
|
||||
import com.google.gerrit.httpd.rpc.dashboard.ListDashboardsServlet;
|
||||
import com.google.gerrit.httpd.rpc.project.ListProjectsServlet;
|
||||
import com.google.gerrit.httpd.rpc.project.ProjectsRestApiServlet;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
@ -95,10 +94,9 @@ class UrlModule extends ServletModule {
|
||||
serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
|
||||
|
||||
filter("/a/*").through(RequireIdentifiedUserFilter.class);
|
||||
serveRegex("^/(?:a/)?accounts/self/capabilities$").with(AccountCapabilitiesServlet.class);
|
||||
serveRegex("^/(?:a/)?changes/$").with(ListChangesServlet.class);
|
||||
serveRegex("^/(?:a/)?projects/(.*)?$").with(ListProjectsServlet.class);
|
||||
serveRegex("^/(?:a/)?dashboards/(.*)?$").with(ListDashboardsServlet.class);
|
||||
serveRegex("^/(?:a/)?accounts/(.*)$").with(AccountsRestApiServlet.class);
|
||||
serveRegex("^/(?:a/)?changes/(.*)$").with(ChangesRestApiServlet.class);
|
||||
serveRegex("^/(?:a/)?projects/(.*)?$").with(ProjectsRestApiServlet.class);
|
||||
|
||||
if (cfg.deprecatedQuery) {
|
||||
serve("/query").with(DeprecatedChangeQueryServlet.class);
|
||||
|
@ -27,7 +27,6 @@ public class HttpPluginModule extends ServletModule {
|
||||
@Override
|
||||
protected void configureServlets() {
|
||||
bind(HttpPluginServlet.class);
|
||||
serve("/plugins/*").with(HttpPluginServlet.class);
|
||||
serveRegex("^/(?:a/)?plugins/(.*)?$").with(HttpPluginServlet.class);
|
||||
|
||||
bind(StartPluginListener.class)
|
||||
|
@ -14,17 +14,19 @@
|
||||
|
||||
package com.google.gerrit.httpd.plugins;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
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.config.CanonicalWebUrl;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.gerrit.server.documentation.MarkdownFormatter;
|
||||
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.StartPluginListener;
|
||||
import com.google.gerrit.server.ssh.SshInfo;
|
||||
@ -80,7 +82,7 @@ class HttpPluginServlet extends HttpServlet
|
||||
private final Cache<ResourceKey, Resource> resourceCache;
|
||||
private final String sshHost;
|
||||
private final int sshPort;
|
||||
private final ListPluginsServlet listServlet;
|
||||
private final RestApiServlet managerApi;
|
||||
|
||||
private List<Plugin> pending = Lists.newArrayList();
|
||||
private String base;
|
||||
@ -92,11 +94,13 @@ class HttpPluginServlet extends HttpServlet
|
||||
@CanonicalWebUrl Provider<String> webUrl,
|
||||
@Named(HttpPluginModule.PLUGIN_RESOURCES) Cache<ResourceKey, Resource> cache,
|
||||
@GerritServerConfig Config cfg,
|
||||
SshInfo sshInfo, ListPluginsServlet listServlet) {
|
||||
SshInfo sshInfo,
|
||||
RestApiServlet.Globals globals,
|
||||
PluginsCollection plugins) {
|
||||
this.mimeUtil = mimeUtil;
|
||||
this.webUrl = webUrl;
|
||||
this.resourceCache = cache;
|
||||
this.listServlet = listServlet;
|
||||
this.managerApi = new RestApiServlet(globals, plugins);
|
||||
|
||||
String sshHost = "review.example.com";
|
||||
int sshPort = 29418;
|
||||
@ -187,11 +191,16 @@ class HttpPluginServlet extends HttpServlet
|
||||
@Override
|
||||
public void service(HttpServletRequest req, HttpServletResponse res)
|
||||
throws IOException, ServletException {
|
||||
String name = extractName(req);
|
||||
if (name.equals("")) {
|
||||
listServlet.service(req, res);
|
||||
List<String> parts = Lists.newArrayList(
|
||||
Splitter.on('/').limit(3).omitEmptyStrings()
|
||||
.split(Strings.nullToEmpty(req.getPathInfo())));
|
||||
|
||||
if (isApiCall(req, parts)) {
|
||||
managerApi.service(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
String name = parts.get(0);
|
||||
final PluginHolder holder = plugins.get(name);
|
||||
if (holder == null) {
|
||||
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,
|
||||
HttpServletRequest req,
|
||||
HttpServletResponse res) throws IOException {
|
||||
@ -553,15 +570,6 @@ class HttpPluginServlet extends HttpServlet
|
||||
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) {
|
||||
res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
|
||||
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
|
||||
// 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_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.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
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.CurrentUser;
|
||||
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.GerritServerConfig;
|
||||
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.project.ChangeControl;
|
||||
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.gson.reflect.TypeToken;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
@ -66,13 +65,10 @@ import com.google.inject.Singleton;
|
||||
import com.jcraft.jsch.HostKey;
|
||||
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
import org.kohsuke.args4j.Option;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.io.Writer;
|
||||
import java.net.URLEncoder;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.Collection;
|
||||
@ -82,8 +78,8 @@ import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ListChanges {
|
||||
private static final Logger log = LoggerFactory.getLogger(ListChanges.class);
|
||||
public class ChangeJson {
|
||||
private static final Logger log = LoggerFactory.getLogger(ChangeJson.class);
|
||||
|
||||
@Singleton
|
||||
static class Urls {
|
||||
@ -104,81 +100,39 @@ public class ListChanges {
|
||||
}
|
||||
}
|
||||
|
||||
private final QueryProcessor imp;
|
||||
private final Provider<ReviewDb> db;
|
||||
private final ApprovalTypes approvalTypes;
|
||||
private final CurrentUser user;
|
||||
private final AnonymousUser anonymous;
|
||||
private final ChangeControl.Factory changeControlFactory;
|
||||
private final ChangeControl.GenericFactory changeControlGenericFactory;
|
||||
private final PatchSetInfoFactory patchSetInfoFactory;
|
||||
private final PatchListCache patchListCache;
|
||||
private final SshInfo sshInfo;
|
||||
private final Provider<String> urlProvider;
|
||||
private final Urls urls;
|
||||
private boolean reverse;
|
||||
private ChangeControl.Factory changeControlUserFactory;
|
||||
private SshInfo sshInfo;
|
||||
private Map<Account.Id, AccountAttribute> accounts;
|
||||
private Map<Change.Id, ChangeControl> controls;
|
||||
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
|
||||
ListChanges(QueryProcessor qp,
|
||||
ChangeJson(
|
||||
Provider<ReviewDb> db,
|
||||
ApprovalTypes at,
|
||||
CurrentUser u,
|
||||
AnonymousUser au,
|
||||
ChangeControl.Factory cf,
|
||||
ChangeControl.GenericFactory gf,
|
||||
PatchSetInfoFactory psi,
|
||||
PatchListCache plc,
|
||||
SshInfo sshInfo,
|
||||
@CanonicalWebUrl Provider<String> curl,
|
||||
Urls urls) {
|
||||
this.imp = qp;
|
||||
this.db = db;
|
||||
this.approvalTypes = at;
|
||||
this.user = u;
|
||||
this.anonymous = au;
|
||||
this.changeControlFactory = cf;
|
||||
this.changeControlGenericFactory = gf;
|
||||
this.patchSetInfoFactory = psi;
|
||||
this.patchListCache = plc;
|
||||
this.sshInfo = sshInfo;
|
||||
this.urlProvider = curl;
|
||||
this.urls = urls;
|
||||
|
||||
@ -187,99 +141,68 @@ public class ListChanges {
|
||||
options = EnumSet.noneOf(ListChangesOption.class);
|
||||
}
|
||||
|
||||
public OutputFormat getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public ListChanges setFormat(OutputFormat fmt) {
|
||||
this.format = fmt;
|
||||
public ChangeJson addOption(ListChangesOption o) {
|
||||
options.add(o);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ListChanges addQuery(String query) {
|
||||
if (queries == null) {
|
||||
queries = Lists.newArrayList();
|
||||
}
|
||||
queries.add(query);
|
||||
public ChangeJson addOptions(Collection<ListChangesOption> o) {
|
||||
options.addAll(o);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void query(Writer out)
|
||||
throws OrmException, QueryParseException, IOException {
|
||||
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");
|
||||
}
|
||||
public ChangeJson setSshInfo(SshInfo info) {
|
||||
sshInfo = info;
|
||||
return this;
|
||||
}
|
||||
|
||||
List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(queries.size());
|
||||
for (String query : queries) {
|
||||
List<ChangeData> changes = imp.queryChanges(query);
|
||||
boolean moreChanges = imp.getLimit() > 0 && changes.size() > imp.getLimit();
|
||||
if (moreChanges) {
|
||||
if (reverse) {
|
||||
changes = changes.subList(1, changes.size());
|
||||
} else {
|
||||
changes = changes.subList(0, imp.getLimit());
|
||||
}
|
||||
}
|
||||
public ChangeJson setChangeControlFactory(ChangeControl.Factory cf) {
|
||||
changeControlUserFactory = cf;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChangeInfo format(ChangeResource rsrc) throws OrmException {
|
||||
return format(new ChangeData(rsrc.getControl()));
|
||||
}
|
||||
|
||||
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.ensureCurrentPatchSetLoaded(db, changes);
|
||||
ChangeData.ensureCurrentApprovalsLoaded(db, 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);
|
||||
res.add(toChangeInfo(changes));
|
||||
}
|
||||
|
||||
if (!accounts.isEmpty()) {
|
||||
for (Account account : db.get().accounts().get(accounts.keySet())) {
|
||||
AccountAttribute a = accounts.get(account.getId());
|
||||
a.name = Strings.emptyToNull(account.getFullName());
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
if (format.isJson()) {
|
||||
format.newGson().toJson(
|
||||
res.size() == 1 ? res.get(0) : res,
|
||||
new TypeToken<List<ChangeInfo>>() {}.getType(),
|
||||
out);
|
||||
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');
|
||||
}
|
||||
}
|
||||
private List<ChangeInfo> toChangeInfo(List<ChangeData> changes)
|
||||
throws OrmException {
|
||||
List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
|
||||
for (ChangeData cd : changes) {
|
||||
info.add(toChangeInfo(cd));
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
private ChangeInfo toChangeInfo(ChangeData cd) throws OrmException {
|
||||
@ -338,7 +261,11 @@ public class ListChanges {
|
||||
}
|
||||
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
@ -577,7 +504,7 @@ public class ListChanges {
|
||||
+ cd.change(db).getProject().get(), refName));
|
||||
}
|
||||
}
|
||||
if (!sshInfo.getHostKeys().isEmpty()) {
|
||||
if (sshInfo != null && !sshInfo.getHostKeys().isEmpty()) {
|
||||
HostKey host = sshInfo.getHostKeys().get(0);
|
||||
r.put("ssh", new FetchInfo(String.format(
|
||||
"ssh://%s/%s",
|
||||
@ -597,14 +524,14 @@ public class ListChanges {
|
||||
return p;
|
||||
}
|
||||
|
||||
static class ChangeInfo {
|
||||
public static class ChangeInfo {
|
||||
final String kind = "gerritcodereview#change";
|
||||
String id;
|
||||
String project;
|
||||
String branch;
|
||||
String topic;
|
||||
String changeId;
|
||||
String subject;
|
||||
public String changeId;
|
||||
public String subject;
|
||||
Change.Status status;
|
||||
Timestamp created;
|
||||
Timestamp updated;
|
||||
@ -618,8 +545,7 @@ public class ListChanges {
|
||||
Map<String, LabelInfo> labels;
|
||||
String current_revision;
|
||||
Map<String, RevisionInfo> revisions;
|
||||
|
||||
Boolean _moreChanges;
|
||||
public Boolean _moreChanges;
|
||||
|
||||
void finish() {
|
||||
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;
|
||||
|
||||
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) {
|
||||
this.changeId = changeId;
|
||||
}
|
||||
|
||||
@Option(name = "--message", aliases = {"-m"},
|
||||
usage = "optional message to append to change")
|
||||
private 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
|
||||
public ReviewResult call() throws EmailException,
|
||||
InvalidChangeOperationException, NoSuchChangeException, OrmException {
|
||||
if (changeId == null) {
|
||||
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;
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
@ -183,6 +183,9 @@ public class GerritGlobalModule extends FactoryModule {
|
||||
factory(FunctionState.Factory.class);
|
||||
|
||||
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);
|
||||
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.RebaseChange;
|
||||
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.BanCommit;
|
||||
import com.google.gerrit.server.git.CreateCodeReviewNotes;
|
||||
@ -66,7 +65,6 @@ public class GerritRequestModule extends FactoryModule {
|
||||
bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
|
||||
bind(MetaDataUpdate.User.class).in(RequestScoped.class);
|
||||
bind(ListProjects.class);
|
||||
bind(ListDashboards.class);
|
||||
bind(ApprovalsUtil.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} */
|
||||
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.
|
||||
* <p>
|
||||
|
@ -106,6 +106,12 @@ public class MetaDataUpdate {
|
||||
getCommitBuilder().setMessage(message);
|
||||
}
|
||||
|
||||
public void setAuthor(IdentifiedUser user) {
|
||||
getCommitBuilder().setAuthor(user.newCommitterIdent(
|
||||
getCommitBuilder().getCommitter().getWhen(),
|
||||
getCommitBuilder().getCommitter().getTimeZone()));
|
||||
}
|
||||
|
||||
/** Close the cached Repository handle. */
|
||||
public void 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.collect.Lists;
|
||||
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.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.inject.Inject;
|
||||
|
||||
@ -28,16 +34,18 @@ import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** List the installed plugins. */
|
||||
public class ListPlugins {
|
||||
public class ListPlugins implements RestReadView<TopLevelResource> {
|
||||
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;
|
||||
|
||||
@Option(name = "--all", aliases = {"-a"}, usage = "List all plugins, including disabled plugins")
|
||||
@ -57,19 +65,26 @@ public class ListPlugins {
|
||||
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);
|
||||
@Override
|
||||
public Object apply(TopLevelResource resource) throws AuthException,
|
||||
BadRequestException, ResourceConflictException, Exception {
|
||||
format = OutputFormat.JSON;
|
||||
return display(null);
|
||||
}
|
||||
|
||||
public JsonElement display(OutputStream displayOutputStream)
|
||||
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();
|
||||
|
||||
List<Plugin> plugins = Lists.newArrayList(pluginLoader.getPlugins(all));
|
||||
Collections.sort(plugins, new Comparator<Plugin>() {
|
||||
@Override
|
||||
@ -80,15 +95,11 @@ public class ListPlugins {
|
||||
|
||||
if (!format.isJson()) {
|
||||
stdout.format("%-30s %-10s %-8s\n", "Name", "Version", "Status");
|
||||
stdout
|
||||
.print("-------------------------------------------------------------------------------\n");
|
||||
stdout.print("-------------------------------------------------------------------------------\n");
|
||||
}
|
||||
|
||||
for (Plugin p : plugins) {
|
||||
PluginInfo info = new PluginInfo();
|
||||
info.version = p.getVersion();
|
||||
info.disabled = p.isDisabled() ? true : null;
|
||||
|
||||
PluginInfo info = new PluginInfo(p);
|
||||
if (format.isJson()) {
|
||||
output.put(p.getName(), info);
|
||||
} 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,
|
||||
new TypeToken<Map<String, PluginInfo>>() {}.getType(), stdout);
|
||||
stdout.print('\n');
|
||||
}
|
||||
stdout.flush();
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class PluginInfo {
|
||||
static class PluginInfo {
|
||||
final String kind = "gerritcodereview#plugin";
|
||||
String id;
|
||||
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;
|
||||
|
||||
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) {
|
||||
if (!all) {
|
||||
return running.values();
|
||||
|
@ -14,10 +14,14 @@
|
||||
|
||||
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.lifecycle.LifecycleModule;
|
||||
|
||||
public class PluginModule extends LifecycleModule {
|
||||
public class PluginModule extends RestApiModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(ServerInformationImpl.class);
|
||||
@ -26,8 +30,20 @@ public class PluginModule extends LifecycleModule {
|
||||
bind(PluginCleanerTask.class);
|
||||
bind(PluginGuiceEnvironment.class);
|
||||
bind(PluginLoader.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;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.gerrit.common.data.GroupReference;
|
||||
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.Project;
|
||||
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.git.GitRepositoryManager;
|
||||
import com.google.gerrit.server.util.TreeFormatter;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.inject.Inject;
|
||||
|
||||
@ -39,11 +47,13 @@ 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.net.URLEncoder;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@ -54,7 +64,7 @@ import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/** 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);
|
||||
|
||||
public static enum FilterType {
|
||||
@ -96,7 +106,8 @@ public class ListProjects {
|
||||
private final GitRepositoryManager repoManager;
|
||||
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;
|
||||
|
||||
@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")
|
||||
private int limit;
|
||||
|
||||
@Option(name = "-p", metaVar = "PERFIX", usage = "match project prefix")
|
||||
private String matchPrefix;
|
||||
|
||||
@Option(name = "--has-acl-for", metaVar = "GROUP", usage =
|
||||
@ -155,7 +167,7 @@ public class ListProjects {
|
||||
}
|
||||
|
||||
public ListProjects setFormat(OutputFormat fmt) {
|
||||
this.format = fmt;
|
||||
format = fmt;
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -164,13 +176,29 @@ public class ListProjects {
|
||||
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);
|
||||
@Override
|
||||
public Object apply(TopLevelResource resource) throws AuthException,
|
||||
BadRequestException, ResourceConflictException, Exception {
|
||||
if (format == OutputFormat.TEXT) {
|
||||
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
||||
display(buf);
|
||||
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;
|
||||
@ -212,8 +240,9 @@ public class ListProjects {
|
||||
&& !rejected.contains(parentState.getProject().getName())) {
|
||||
ProjectControl parentCtrl = parentState.controlFor(currentUser);
|
||||
if (parentCtrl.isVisible() || parentCtrl.isOwner()) {
|
||||
info.name = parentState.getProject().getName();
|
||||
info.description = parentState.getProject().getDescription();
|
||||
info.setName(parentState.getProject().getName());
|
||||
info.description = Strings.emptyToNull(
|
||||
parentState.getProject().getDescription());
|
||||
} else {
|
||||
rejected.add(parentState.getProject().getName());
|
||||
continue;
|
||||
@ -236,7 +265,7 @@ public class ListProjects {
|
||||
continue;
|
||||
}
|
||||
|
||||
info.name = projectName.get();
|
||||
info.setName(projectName.get());
|
||||
if (showTree && format.isJson()) {
|
||||
ProjectState parent = e.getParentState();
|
||||
if (parent != null) {
|
||||
@ -252,8 +281,8 @@ public class ListProjects {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showDescription && !e.getProject().getDescription().isEmpty()) {
|
||||
info.description = e.getProject().getDescription();
|
||||
if (showDescription) {
|
||||
info.description = Strings.emptyToNull(e.getProject().getDescription());
|
||||
}
|
||||
|
||||
try {
|
||||
@ -305,7 +334,7 @@ public class ListProjects {
|
||||
break;
|
||||
}
|
||||
|
||||
if (format.isJson()) {
|
||||
if (stdout == null || format.isJson()) {
|
||||
output.put(info.name, info);
|
||||
continue;
|
||||
}
|
||||
@ -330,15 +359,22 @@ public class ListProjects {
|
||||
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(
|
||||
output, new TypeToken<Map<String, ProjectInfo>>() {}.getType(), stdout);
|
||||
stdout.print('\n');
|
||||
} else if (showTree && treeMap.size() > 0) {
|
||||
printProjectTree(stdout, treeMap);
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
stdout.flush();
|
||||
if (stdout != null) {
|
||||
stdout.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -408,12 +444,23 @@ public class ListProjects {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static class ProjectInfo {
|
||||
static class ProjectInfo {
|
||||
@SuppressWarnings("unused")
|
||||
final String kind = "gerritcodereview#project";
|
||||
|
||||
transient String name;
|
||||
String id;
|
||||
String parent;
|
||||
String description;
|
||||
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;
|
||||
}
|
||||
|
||||
public ChangeData(final ChangeControl c) {
|
||||
legacyId = c.getChange().getId();
|
||||
change = c.getChange();
|
||||
changeControl = c;
|
||||
}
|
||||
|
||||
public void setCurrentFilePaths(String[] filePaths) {
|
||||
currentFiles = filePaths;
|
||||
}
|
||||
@ -191,7 +197,7 @@ public class ChangeData {
|
||||
return visibleTo == user;
|
||||
}
|
||||
|
||||
ChangeControl changeControl() {
|
||||
public ChangeControl 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>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
|
@ -34,6 +34,9 @@
|
||||
|
||||
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.Injector;
|
||||
import com.google.inject.Key;
|
||||
@ -54,11 +57,9 @@ import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Extended command line parser which handles --foo=value arguments.
|
||||
@ -186,7 +187,7 @@ public class CmdLineParser {
|
||||
}
|
||||
|
||||
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++) {
|
||||
final String str = args[argi];
|
||||
if (str.equals("--")) {
|
||||
@ -211,15 +212,20 @@ public class CmdLineParser {
|
||||
|
||||
public void parseOptionMap(Map<String, String[]> parameters)
|
||||
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,
|
||||
Set<String> argNames)
|
||||
public void parseOptionMap(Multimap<String, String> params)
|
||||
throws CmdLineException {
|
||||
ArrayList<String> tmp = new ArrayList<String>();
|
||||
for (Map.Entry<String, String[]> ent : parameters.entrySet()) {
|
||||
String name = ent.getKey();
|
||||
List<String> tmp = Lists.newArrayListWithCapacity(2 * params.size());
|
||||
for (final String key : params.keySet()) {
|
||||
String name = key;
|
||||
if (!name.startsWith("-")) {
|
||||
if (name.length() == 1) {
|
||||
name = "-" + name;
|
||||
@ -230,17 +236,15 @@ public class CmdLineParser {
|
||||
|
||||
if (findHandler(name) instanceof BooleanOptionHandler) {
|
||||
boolean on = false;
|
||||
for (String value : ent.getValue()) {
|
||||
on = toBoolean(ent.getKey(), value);
|
||||
for (String value : params.get(key)) {
|
||||
on = toBoolean(key, value);
|
||||
}
|
||||
if (on) {
|
||||
tmp.add(name);
|
||||
}
|
||||
} else {
|
||||
for (String value : ent.getValue()) {
|
||||
if (!argNames.contains(ent.getKey())) {
|
||||
tmp.add(name);
|
||||
}
|
||||
for (String value : params.get(key)) {
|
||||
tmp.add(name);
|
||||
tmp.add(value);
|
||||
}
|
||||
}
|
||||
@ -328,7 +332,7 @@ public class CmdLineParser {
|
||||
private void ensureOptionsInitialized() {
|
||||
if (options == null) {
|
||||
help = new HelpOption();
|
||||
options = new ArrayList<OptionHandler>();
|
||||
options = Lists.newArrayList();
|
||||
addOption(help, help);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user