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:
Shawn O. Pearce 2012-11-16 10:57:31 -08:00
parent 401440f975
commit ea6d0b5a27
83 changed files with 4477 additions and 1559 deletions

View File

@ -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
------

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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> {
}

View File

@ -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 {
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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&lt;RestView&lt;MyResource&gt;&gt; MY_KIND =
* new TypeLiteral&lt;RestView&lt;MyResource&gt;&gt;() {};
* }
*
* public class MyModule extends AbstractModule {
* &#064;Override
* protected void configure() {
* DynamicMap.mapOf(binder(), MyResource.MY_KIND);
*
* get(MyResource.MY_KIND, &quot;action&quot;).to(MyAction.class);
* }
* }
*
* public class MyCollection extends RestCollection&lt;TopLevelResource, MyResource&gt; {
* private final DynamicMap&lt;RestView&lt;MyResource&gt;&gt; views;
*
* &#064;Inject
* MyCollection(DynamicMap&lt;RestView&lt;MyResource&gt;&gt; views) {
* this.views = views;
* }
*
* public DynamicMap&lt;RestView&lt;MyResource&gt;&gt; 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();
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 {
}

View File

@ -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> {
}

View File

@ -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() {
}
}

View File

@ -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);
}
});

View File

@ -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() {
}

View File

@ -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() {
}
}

View File

@ -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());
}
}

View File

@ -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));
}

View File

@ -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 &lt;script src="...&gt; 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);
}
}
}

View File

@ -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);

View File

@ -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)

View File

@ -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");

View File

@ -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");
}
}

View File

@ -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 &lt;script src="...&gt; 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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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";
}
}

View File

@ -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 {

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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<?, ?>>() {});

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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();

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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");
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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()));
}
}
}

View File

@ -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()));
}
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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);
}
}