Skip to content

Commit 62e9979

Browse files
author
Vladimir Kotal
authored
implement history API endpoint (#3045)
fixes #3044
1 parent e26546b commit 62e9979

File tree

5 files changed

+439
-34
lines changed

5 files changed

+439
-34
lines changed

apiary.apib

Lines changed: 85 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ OpenGrok RESTful API documentation. The following endpoints are accessible under
66

77
Besides `/suggester` and `/search` endpoints, everything is accessible from `localhost` only.
88

9+
## Authorization framework reload [/configuration/authorization/reload]
10+
11+
### reloads authorization framework [POST]
12+
13+
+ Request (application/text)
14+
15+
+ Response 204
16+
917
## Configuration [/configuration]
1018

1119
### return XML representation of configuration [GET]
@@ -59,11 +67,72 @@ Besides `/suggester` and `/search` endpoints, everything is accessible from `loc
5967

6068
+ Response 204
6169

62-
## Authorization framework reload [/configuration/authorization/reload]
70+
## History [/history{?path,withFiles,start,max}]
6371

64-
### reloads authorization framework [POST]
72+
### get history entries [GET]
73+
74+
+ Parameters
75+
+ path (string) - path of file/directory to get history for, relative to source root
76+
+ withFiles (optional, boolean) - whether to include list of files
77+
+ start (optional, int) - start index
78+
+ max (optional, int) - number of entries to get (default value 1000)
79+
80+
+ Response 200 (application/json)
81+
+ Body
82+
83+
{
84+
"entries": [
85+
{
86+
"revision": "86b0ab6b",
87+
"date": 1565163646000,
88+
"author": "Adam Hornacek <[email protected]>",
89+
"tags": null,
90+
"message": "Try to use mvnw in CI",
91+
"files": [
92+
"/opengrok/docker/README.md"
93+
]
94+
},
95+
{
96+
"revision": "a391bead",
97+
"date": 1564745178000,
98+
"author": "Adam Hornacek <[email protected]>",
99+
"tags": null,
100+
"message": "Fix docker readme typo",
101+
"files": [
102+
"/opengrok/docker/README.md"
103+
]
104+
},
105+
{
106+
"revision": "33551f14",
107+
"date": 1563531391000,
108+
"author": "Vladimir Kotal <[email protected]>",
109+
"tags": "1.3.0, 1.2.25, 1.2.24",
110+
"message": "document docker-compose",
111+
"files": [
112+
"/opengrok/docker/README.md"
113+
]
114+
}
115+
],
116+
"start": 5,
117+
"count": 3,
118+
"total": 24
119+
}
65120

66-
+ Request (application/text)
121+
122+
## Include files reload [/system/includes/reload]
123+
124+
### reloads all include files for web application [PUT]
125+
126+
+ Response 204
127+
128+
## Index searchers refresh [/system/refresh]
129+
130+
### refreshes index searchers for specified project [PUT]
131+
132+
+ Request (text/plain)
133+
+ Body
134+
135+
project name to refresh
67136

68137
+ Response 204
69138

@@ -122,6 +191,18 @@ see https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html#parse-java
122191
}
123192
]
124193

194+
## Path descriptions [/system/pathdesc]
195+
196+
### updates path descriptions for web application [POST]
197+
198+
+ Request (text/plain)
199+
+ Body
200+
201+
/foo bar
202+
/foo/foo bar bar
203+
204+
+ Response 204
205+
125206
## Projects [/projects]
126207

127208
For all entry points that modify web application configuration it is worth noting that
@@ -266,7 +347,7 @@ The repository path is relative to source root.
266347
+ hist (optional, string) - history field value to search for
267348
+ type (optional, string) - type of the files to search for
268349
+ projects (optional, string) - projects to search in
269-
+ maxresults (optional, string) - maximum number of documents whose hits will be returned
350+
+ maxresults (optional, string) - maximum number of documents whose hits will be returned (default 1000)
270351
+ start (optional, string) - start index from which to return results
271352

272353
+ Response 200 (application/json)
@@ -427,33 +508,4 @@ This kicks off suggester data rebuild in the background, i.e. the rebuild will v
427508
+ Parameters
428509
+ project - project name
429510

430-
+ Response 204
431-
432-
## Index searchers refresh [/system/refresh]
433-
434-
### refreshes index searchers for specified project [PUT]
435-
436-
+ Request (text/plain)
437-
+ Body
438-
439-
project name to refresh
440-
441-
+ Response 204
442-
443-
## Include files reload [/system/includes/reload]
444-
445-
### reloads all include files for web application [PUT]
446-
447-
+ Response 204
448-
449-
## Path descriptions [/system/pathdesc]
450-
451-
### updates path descriptions for web application [POST]
452-
453-
+ Request (text/plain)
454-
+ Body
455-
456-
/foo bar
457-
/foo/foo bar bar
458-
459511
+ Response 204

dev/main

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ ret=0
4444
./mvnw -B -V verify $extra_args || ret=1
4545

4646
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
47+
echo "Checking Apiary blueprint format"
4748
node dev/parse.js || ret=1
4849
fi
4950

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
* CDDL HEADER START
3+
*
4+
* The contents of this file are subject to the terms of the
5+
* Common Development and Distribution License (the "License").
6+
* You may not use this file except in compliance with the License.
7+
*
8+
* See LICENSE.txt included in this distribution for the specific
9+
* language governing permissions and limitations under the License.
10+
*
11+
* When distributing Covered Code, include this CDDL HEADER in each
12+
* file and include the License file at LICENSE.txt.
13+
* If applicable, add the following below this CDDL HEADER, with the
14+
* fields enclosed by brackets "[]" replaced with your own identifying
15+
* information: Portions Copyright [yyyy] [name of copyright owner]
16+
*
17+
* CDDL HEADER END
18+
*/
19+
20+
/*
21+
* Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
22+
*/
23+
24+
package org.opengrok.web.api.v1.controller;
25+
26+
import com.fasterxml.jackson.annotation.JsonProperty;
27+
import org.opengrok.indexer.authorization.AuthorizationFramework;
28+
import org.opengrok.indexer.configuration.Project;
29+
import org.opengrok.indexer.configuration.RuntimeEnvironment;
30+
import org.opengrok.indexer.history.History;
31+
import org.opengrok.indexer.history.HistoryEntry;
32+
import org.opengrok.indexer.history.HistoryException;
33+
import org.opengrok.indexer.history.HistoryGuru;
34+
import org.opengrok.indexer.web.messages.JSONable;
35+
import org.opengrok.web.api.v1.filter.CorsEnable;
36+
37+
import javax.servlet.http.HttpServletRequest;
38+
import javax.servlet.http.HttpServletResponse;
39+
import javax.ws.rs.DefaultValue;
40+
import javax.ws.rs.GET;
41+
import javax.ws.rs.Path;
42+
import javax.ws.rs.Produces;
43+
import javax.ws.rs.QueryParam;
44+
import javax.ws.rs.core.Context;
45+
import javax.ws.rs.core.MediaType;
46+
import javax.ws.rs.core.Response;
47+
import java.io.File;
48+
import java.io.IOException;
49+
import java.util.ArrayList;
50+
import java.util.Date;
51+
import java.util.List;
52+
import java.util.Objects;
53+
import java.util.SortedSet;
54+
55+
@Path(HistoryController.PATH)
56+
public final class HistoryController {
57+
58+
private static final RuntimeEnvironment env = RuntimeEnvironment.getInstance();
59+
60+
private static final int MAX_RESULTS = 1000;
61+
62+
public static final String PATH = "/history";
63+
64+
static class HistoryEntryDTO implements JSONable {
65+
@JsonProperty
66+
private String revision;
67+
@JsonProperty
68+
private Date date;
69+
@JsonProperty
70+
private String author;
71+
@JsonProperty
72+
private String tags;
73+
@JsonProperty
74+
private String message;
75+
@JsonProperty
76+
private SortedSet<String> files;
77+
78+
// for testing
79+
HistoryEntryDTO() {
80+
}
81+
82+
HistoryEntryDTO(HistoryEntry entry) {
83+
this.revision = entry.getRevision();
84+
this.date = entry.getDate();
85+
this.author = entry.getAuthor();
86+
this.tags = entry.getTags();
87+
this.message = entry.getMessage();
88+
this.files = entry.getFiles();
89+
}
90+
91+
// for testing
92+
public String getAuthor() {
93+
return author;
94+
}
95+
96+
@Override
97+
public boolean equals(Object obj) {
98+
if (obj == null) {
99+
return false;
100+
}
101+
if (getClass() != obj.getClass()) {
102+
return false;
103+
}
104+
final HistoryEntryDTO other = (HistoryEntryDTO) obj;
105+
if (!Objects.equals(this.revision, other.revision)) {
106+
return false;
107+
}
108+
if (!Objects.equals(this.date, other.date)) {
109+
return false;
110+
}
111+
if (!Objects.equals(this.author, other.author)) {
112+
return false;
113+
}
114+
if (!Objects.equals(this.tags, other.tags)) {
115+
return false;
116+
}
117+
if (!Objects.equals(this.message, other.message)) {
118+
return false;
119+
}
120+
if (!Objects.equals(this.files, other.files)) {
121+
return false;
122+
}
123+
return true;
124+
}
125+
126+
@Override
127+
public int hashCode() {
128+
return Objects.hash(revision, date, author, tags, message, files);
129+
}
130+
}
131+
132+
static class HistoryDTO implements JSONable {
133+
@JsonProperty
134+
private final List<HistoryEntryDTO> entries;
135+
@JsonProperty
136+
private int start;
137+
@JsonProperty
138+
private int count;
139+
@JsonProperty
140+
private int total;
141+
142+
// for testing
143+
HistoryDTO() {
144+
this.entries = new ArrayList<>();
145+
}
146+
147+
HistoryDTO(List<HistoryEntryDTO> entries, int start, int count, int total) {
148+
this.entries = entries;
149+
this.start = start;
150+
this.count = count;
151+
this.total = total;
152+
}
153+
154+
// for testing
155+
public List<HistoryEntryDTO> getEntries() {
156+
return entries;
157+
}
158+
159+
public boolean equals(Object obj) {
160+
if (obj == null) {
161+
return false;
162+
}
163+
if (getClass() != obj.getClass()) {
164+
return false;
165+
}
166+
final HistoryDTO other = (HistoryDTO) obj;
167+
if (!Objects.equals(this.entries, other.entries)) {
168+
return false;
169+
}
170+
if (!Objects.equals(this.start, other.start)) {
171+
return false;
172+
}
173+
if (!Objects.equals(this.count, other.count)) {
174+
return false;
175+
}
176+
if (!Objects.equals(this.total, other.total)) {
177+
return false;
178+
}
179+
return true;
180+
}
181+
182+
@Override
183+
public int hashCode() {
184+
return Objects.hash(entries, start, count, total);
185+
}
186+
}
187+
188+
static HistoryDTO getHistoryDTO(List<HistoryEntry> historyEntries, int start, int count, int total) {
189+
List<HistoryEntryDTO> entries = new ArrayList<>();
190+
historyEntries.stream().map(HistoryEntryDTO::new).forEach(entries::add);
191+
return new HistoryDTO(entries, start, count, total);
192+
}
193+
194+
@GET
195+
@CorsEnable
196+
@Produces(MediaType.APPLICATION_JSON)
197+
public HistoryDTO get(@Context HttpServletRequest request,
198+
@Context HttpServletResponse response,
199+
@QueryParam("path") final String path,
200+
@QueryParam("withFiles") final boolean withFiles,
201+
@QueryParam("max") @DefaultValue(MAX_RESULTS + "") final int maxEntries,
202+
@QueryParam("start") @DefaultValue(0 + "") final int startIndex)
203+
throws HistoryException, IOException {
204+
205+
if (request != null) {
206+
AuthorizationFramework auth = env.getAuthorizationFramework();
207+
if (auth != null) {
208+
Project p = Project.getProject(path.startsWith("/") ? path : "/" + path);
209+
if (p != null && !auth.isAllowed(request, p)) {
210+
response.sendError(Response.status(Response.Status.FORBIDDEN).build().getStatus(),
211+
"not authorized");
212+
return null;
213+
}
214+
}
215+
}
216+
217+
History history = HistoryGuru.getInstance().getHistory(new File(env.getSourceRootFile(), path),
218+
withFiles, true);
219+
if (history == null) {
220+
return null;
221+
}
222+
223+
return getHistoryDTO(history.getHistoryEntries(maxEntries, startIndex),
224+
startIndex, maxEntries, history.getHistoryEntries().size());
225+
}
226+
}

0 commit comments

Comments
 (0)