Merged in feature/search (pull request #231)

feature/search
This commit is contained in:
Sebastian Sdorra
2019-04-24 11:51:57 +00:00
68 changed files with 872 additions and 2399 deletions

View File

@@ -35,6 +35,7 @@ package sonia.scm;
import java.util.Collection;
import java.util.Comparator;
import java.util.function.Predicate;
/**
* Base interface for all manager classes.
@@ -82,11 +83,12 @@ public interface Manager<T extends ModelObject>
* Returns all object of the store sorted by the given {@link java.util.Comparator}
*
*
* @param filter to filter the returned objects
* @param comparator to sort the returned objects
* @since 1.4
* @return all object of the store sorted by the given {@link java.util.Comparator}
*/
Collection<T> getAll(Comparator<T> comparator);
Collection<T> getAll(Predicate<T> filter, Comparator<T> comparator);
/**
* Returns objects from the store which are starts at the given start
@@ -125,6 +127,7 @@ public interface Manager<T extends ModelObject>
* <p>This default implementation reads all items, first, so you might want to adapt this
* whenever reading is expensive!</p>
*
* @param filter to filter returned objects
* @param comparator to sort the returned objects
* @param pageNumber the number of the page to be returned (zero based)
* @param pageSize the size of the pages
@@ -134,8 +137,8 @@ public interface Manager<T extends ModelObject>
* page. If the requested page number exceeds the existing pages, an
* empty page result is returned.
*/
default PageResult<T> getPage(Comparator<T> comparator, int pageNumber, int pageSize) {
return PageResult.createPage(getAll(comparator), pageNumber, pageSize);
default PageResult<T> getPage(Predicate<T> filter, Comparator<T> comparator, int pageNumber, int pageSize) {
return PageResult.createPage(getAll(filter, comparator), pageNumber, pageSize);
}
}

View File

@@ -37,6 +37,7 @@ package sonia.scm;
import java.io.IOException;
import java.util.Collection;
import java.util.Comparator;
import java.util.function.Predicate;
/**
* Basic decorator for manager classes.
@@ -104,9 +105,9 @@ public class ManagerDecorator<T extends ModelObject> implements Manager<T> {
}
@Override
public Collection<T> getAll(Comparator<T> comparator)
public Collection<T> getAll(Predicate<T> filter, Comparator<T> comparator)
{
return decorated.getAll(comparator);
return decorated.getAll(filter, comparator);
}
@Override

View File

@@ -92,8 +92,6 @@ public final class SearchUtil
{
result = true;
if (Util.isNotEmpty(other))
{
for (String o : other)
{
if ((o == null) ||!o.matches(query))
@@ -104,7 +102,6 @@ public final class SearchUtil
}
}
}
}
return result;
}
@@ -126,8 +123,6 @@ public final class SearchUtil
String query = createStringQuery(request);
if (!value.matches(query))
{
if (Util.isNotEmpty(other))
{
for (String o : other)
{
@@ -139,7 +134,6 @@ public final class SearchUtil
}
}
}
}
else
{
result = true;

View File

@@ -5,6 +5,7 @@ import org.mockito.Mock;
import java.util.Collection;
import java.util.Comparator;
import java.util.function.Predicate;
import java.util.stream.IntStream;
import static java.util.stream.Collectors.toList;
@@ -18,21 +19,22 @@ public class ManagerTest {
@Mock
private Comparator comparator;
private Predicate predicate = x -> true;
@Test(expected = IllegalArgumentException.class)
public void validatesPageNumber() {
manager.getPage(comparator, -1, 5);
manager.getPage(predicate, comparator, -1, 5);
}
@Test(expected = IllegalArgumentException.class)
public void validatesPageSize() {
manager.getPage(comparator, 2, 0);
manager.getPage(predicate, comparator, 2, 0);
}
@Test
public void getsNoPage() {
givenItemCount = 0;
PageResult singlePage = manager.getPage(comparator, 0, 5);
PageResult singlePage = manager.getPage(predicate, comparator, 0, 5);
assertEquals(0, singlePage.getEntities().size());
assertEquals(givenItemCount, singlePage.getOverallCount());
}
@@ -40,7 +42,7 @@ public class ManagerTest {
@Test
public void getsSinglePageWithoutEnoughItems() {
givenItemCount = 3;
PageResult singlePage = manager.getPage(comparator, 0, 4);
PageResult singlePage = manager.getPage(predicate, comparator, 0, 4);
assertEquals(3, singlePage.getEntities().size() );
assertEquals(givenItemCount, singlePage.getOverallCount());
}
@@ -48,7 +50,7 @@ public class ManagerTest {
@Test
public void getsSinglePageWithExactCountOfItems() {
givenItemCount = 3;
PageResult singlePage = manager.getPage(comparator, 0, 3);
PageResult singlePage = manager.getPage(predicate, comparator, 0, 3);
assertEquals(3, singlePage.getEntities().size() );
assertEquals(givenItemCount, singlePage.getOverallCount());
}
@@ -56,11 +58,11 @@ public class ManagerTest {
@Test
public void getsTwoPages() {
givenItemCount = 3;
PageResult page1 = manager.getPage(comparator, 0, 2);
PageResult page1 = manager.getPage(predicate, comparator, 0, 2);
assertEquals(2, page1.getEntities().size());
assertEquals(givenItemCount, page1.getOverallCount());
PageResult page2 = manager.getPage(comparator, 1, 2);
PageResult page2 = manager.getPage(predicate, comparator, 1, 2);
assertEquals(1, page2.getEntities().size());
assertEquals(givenItemCount, page2.getOverallCount());
}
@@ -79,7 +81,7 @@ public class ManagerTest {
}
@Override
public Collection getAll(Comparator comparator) { return getAll(); }
public Collection getAll(Predicate filter, Comparator comparator) { return getAll(); }
@Override
public Collection getAll(int start, int limit) { return null; }

View File

@@ -12,6 +12,6 @@
"@scm-manager/ui-extensions": "^0.1.2"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.26"
"@scm-manager/ui-bundler": "^0.0.28"
}
}

View File

@@ -707,9 +707,9 @@
version "0.0.2"
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
"@scm-manager/ui-bundler@^0.0.26":
version "0.0.26"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66"
"@scm-manager/ui-bundler@^0.0.28":
version "0.0.28"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e"
dependencies:
"@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0"

View File

@@ -9,6 +9,6 @@
"@scm-manager/ui-extensions": "^0.1.2"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.26"
"@scm-manager/ui-bundler": "^0.0.28"
}
}

View File

@@ -641,9 +641,9 @@
version "0.0.2"
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
"@scm-manager/ui-bundler@^0.0.26":
version "0.0.26"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66"
"@scm-manager/ui-bundler@^0.0.28":
version "0.0.28"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e"
dependencies:
"@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0"

View File

@@ -9,6 +9,6 @@
"@scm-manager/ui-extensions": "^0.1.2"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.26"
"@scm-manager/ui-bundler": "^0.0.28"
}
}

View File

@@ -641,9 +641,9 @@
version "0.0.2"
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
"@scm-manager/ui-bundler@^0.0.26":
version "0.0.26"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66"
"@scm-manager/ui-bundler@^0.0.28":
version "0.0.28"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e"
dependencies:
"@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0"

View File

@@ -14,7 +14,7 @@
"eslint-fix": "eslint src --fix"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.26",
"@scm-manager/ui-bundler": "^0.0.28",
"create-index": "^2.3.0",
"enzyme": "^3.5.0",
"enzyme-adapter-react-16": "^1.3.1",
@@ -30,6 +30,7 @@
"@scm-manager/ui-types": "2.0.0-SNAPSHOT",
"classnames": "^2.2.6",
"moment": "^2.22.2",
"query-string": "5",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-diff-view": "^1.8.1",

View File

@@ -1,18 +1,26 @@
//@flow
import React from "react";
import {translate} from "react-i18next";
import type {PagedCollection} from "@scm-manager/ui-types";
import {Button} from "./buttons";
import { translate } from "react-i18next";
import type { PagedCollection } from "@scm-manager/ui-types";
import { Button } from "./buttons";
type Props = {
collection: PagedCollection,
page: number,
filter?: string,
// context props
t: string => string
};
class LinkPaginator extends React.Component<Props> {
addFilterToLink(link: string) {
const { filter } = this.props;
if (filter) {
return `${link}?q=${filter}`;
}
return link;
}
renderFirstButton() {
return (
@@ -20,7 +28,7 @@ class LinkPaginator extends React.Component<Props> {
className={"pagination-link"}
label={"1"}
disabled={false}
link={"1"}
link={this.addFilterToLink("1")}
/>
);
}
@@ -34,7 +42,7 @@ class LinkPaginator extends React.Component<Props> {
className={className}
label={label ? label : previousPage.toString()}
disabled={!this.hasLink("prev")}
link={`${previousPage}`}
link={this.addFilterToLink(`${previousPage}`)}
/>
);
}
@@ -52,7 +60,7 @@ class LinkPaginator extends React.Component<Props> {
className={className}
label={label ? label : nextPage.toString()}
disabled={!this.hasLink("next")}
link={`${nextPage}`}
link={this.addFilterToLink(`${nextPage}`)}
/>
);
}
@@ -64,7 +72,7 @@ class LinkPaginator extends React.Component<Props> {
className={"pagination-link"}
label={`${collection.pageTotal}`}
disabled={false}
link={`${collection.pageTotal}`}
link={this.addFilterToLink(`${collection.pageTotal}`)}
/>
);
}
@@ -115,10 +123,15 @@ class LinkPaginator extends React.Component<Props> {
}
render() {
const { t } = this.props;
const { collection, t } = this.props;
if(collection) {
return (
<nav className="pagination is-centered" aria-label="pagination">
{this.renderPreviousButton("pagination-previous", t("paginator.previous"))}
{this.renderPreviousButton(
"pagination-previous",
t("paginator.previous")
)}
<ul className="pagination-list">
{this.pageLinks().map((link, index) => {
return <li key={index}>{link}</li>;
@@ -128,6 +141,8 @@ class LinkPaginator extends React.Component<Props> {
</nav>
);
}
return null;
}
}
export default translate("commons")(LinkPaginator);

View File

@@ -0,0 +1,58 @@
// @flow
import React from "react";
import type { History } from "history";
import { withRouter } from "react-router-dom";
import classNames from "classnames";
import injectSheet from "react-jss";
import { FilterInput } from "./forms";
import { Button, urls } from "./index";
type Props = {
showCreateButton: boolean,
link: string,
label?: string,
// context props
classes: Object,
history: History,
location: any
};
const styles = {
button: {
float: "right",
marginTop: "1.25rem",
marginLeft: "1.25rem"
}
};
class OverviewPageActions extends React.Component<Props> {
render() {
const { history, location, link } = this.props;
return (
<>
<FilterInput
value={urls.getQueryStringFromLocation(location)}
filter={filter => {
history.push(`/${link}/?q=${filter}`);
}}
/>
{this.renderCreateButton()}
</>
);
}
renderCreateButton() {
const { showCreateButton, classes, link, label } = this.props;
if (showCreateButton) {
return (
<div className={classNames(classes.button, "input-button control")}>
<Button label={label} link={`/${link}/create`} color="primary" />
</div>
);
}
return null;
}
}
export default injectSheet(styles)(withRouter(OverviewPageActions));

View File

@@ -0,0 +1,69 @@
//@flow
import React from "react";
import injectSheet from "react-jss";
import classNames from "classnames";
import { translate } from "react-i18next";
type Props = {
filter: string => void,
value?: string,
// context props
classes: Object,
t: string => string
};
type State = {
value: string
};
const styles = {
inputField: {
float: "right",
marginTop: "1.25rem"
},
inputHeight: {
height: "2.5rem"
}
};
class FilterInput extends React.Component<Props, State> {
constructor(props) {
super(props);
this.state = { value: this.props.value ? this.props.value : "" };
}
handleChange = event => {
this.setState({ value: event.target.value });
};
handleSubmit = event => {
this.props.filter(this.state.value);
event.preventDefault();
};
render() {
const { classes, t } = this.props;
return (
<form
className={classNames(classes.inputField, "input-field")}
onSubmit={this.handleSubmit}
>
<div className="control has-icons-left">
<input
className={classNames(classes.inputHeight, "input")}
type="search"
placeholder={t("filterEntries")}
value={this.state.value}
onChange={this.handleChange}
/>
<span className="icon is-small is-left">
<i className="fas fa-search" />
</span>
</div>
</form>
);
}
}
export default injectSheet(styles)(translate("commons")(FilterInput));

View File

@@ -5,6 +5,7 @@ export { default as AutocompleteAddEntryToTableField } from "./AutocompleteAddEn
export { default as MemberNameTable } from "./MemberNameTable.js";
export { default as Checkbox } from "./Checkbox.js";
export { default as Radio } from "./Radio.js";
export { default as FilterInput } from "./FilterInput.js";
export { default as InputField } from "./InputField.js";
export { default as Select } from "./Select.js";
export { default as Textarea } from "./Textarea.js";

View File

@@ -22,12 +22,14 @@ export { default as ProtectedRoute } from "./ProtectedRoute.js";
export { default as Help } from "./Help";
export { default as HelpIcon } from "./HelpIcon";
export { default as Tooltip } from "./Tooltip";
// TODO do we need this? getPageFromMatch is already exported by urls
export { getPageFromMatch } from "./urls";
export { default as Autocomplete} from "./Autocomplete";
export { default as BranchSelector } from "./BranchSelector";
export { default as MarkdownView } from "./MarkdownView";
export { default as SyntaxHighlighter } from "./SyntaxHighlighter";
export { default as ErrorBoundary } from "./ErrorBoundary";
export { default as OverviewPageActions } from "./OverviewPageActions.js";
export { apiClient } from "./apiclient.js";
export * from "./errors";

View File

@@ -1,11 +1,11 @@
//@flow
import * as React from "react";
import injectSheet from "react-jss";
import classNames from "classnames";
import Loading from "./../Loading";
import ErrorNotification from "./../ErrorNotification";
import Title from "./Title";
import Subtitle from "./Subtitle";
import injectSheet from "react-jss";
import classNames from "classnames";
import PageActions from "./PageActions";
import ErrorBoundary from "../ErrorBoundary";
@@ -22,9 +22,9 @@ type Props = {
};
const styles = {
spacing: {
marginTop: "1.25rem",
textAlign: "right"
actions: {
display: "flex",
justifyContent: "flex-end"
}
};
@@ -36,47 +36,45 @@ class Page extends React.Component<Props> {
<div className="container">
{this.renderPageHeader()}
<ErrorBoundary>
<ErrorNotification error={error}/>
<ErrorNotification error={error} />
{this.renderContent()}
</ErrorBoundary>
</div>
</section>
);
}
renderPageHeader() {
const { title, subtitle, children, classes } = this.props;
const { error, title, subtitle, children, classes } = this.props;
let pageActions = null;
let pageActionsExists = false;
React.Children.forEach(children, child => {
if (child && child.type.name === PageActions.name) {
if (child && !error) {
if (child.type.name === PageActions.name)
pageActions = (
<div className="column is-two-fifths">
<div
className={classNames(
classes.spacing,
"is-mobile-create-button-spacing"
classes.actions,
"column is-three-fifths is-mobile-action-spacing"
)}
>
{child}
</div>
</div>
);
pageActionsExists = true;
}
});
let underline = pageActionsExists ? (
<hr className="header-with-actions"/>
<hr className="header-with-actions" />
) : null;
return (
<>
<div className="columns">
<div className="column">
<Title title={title}/>
<Subtitle subtitle={subtitle}/>
<Title title={title} />
<Subtitle subtitle={subtitle} />
</div>
{pageActions}
</div>
@@ -92,14 +90,16 @@ class Page extends React.Component<Props> {
return null;
}
if (loading) {
return <Loading/>;
return <Loading />;
}
let content = [];
React.Children.forEach(children, child => {
if (child && child.type.name !== PageActions.name) {
if (child) {
if (child.type.name !== PageActions.name) {
content.push(child);
}
}
});
return content;
}

View File

@@ -7,12 +7,11 @@ import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
type Props = {
t: string => string,
links: Links,
links: Links
};
class PrimaryNavigation extends React.Component<Props> {
createNavigationAppender = (navigationItems) => {
createNavigationAppender = navigationItems => {
const { t, links } = this.props;
return (to: string, match: string, label: string, linkName: string) => {
@@ -24,8 +23,8 @@ class PrimaryNavigation extends React.Component<Props> {
match={match}
label={t(label)}
key={linkName}
/>)
;
/>
);
navigationItems.push(navigationItem);
}
};
@@ -63,16 +62,26 @@ class PrimaryNavigation extends React.Component<Props> {
<ExtensionPoint name="primary-navigation.first-menu" props={props} />
);
}
append("/repos", "/(repo|repos)", "primary-navigation.repositories", "repositories");
append("/users", "/(user|users)", "primary-navigation.users", "users");
append("/groups", "/(group|groups)", "primary-navigation.groups", "groups");
append(
"/repos/",
"/(repo|repos)",
"primary-navigation.repositories",
"repositories"
);
append("/users/", "/(user|users)", "primary-navigation.users", "users");
append(
"/groups/",
"/(group|groups)",
"primary-navigation.groups",
"groups"
);
append("/config", "/config", "primary-navigation.config", "config");
navigationItems.push(
<ExtensionPoint
name="primary-navigation"
renderAll={true}
props={{links: this.props.links}}
props={{ links: this.props.links }}
/>
);
@@ -86,9 +95,7 @@ class PrimaryNavigation extends React.Component<Props> {
return (
<nav className="tabs is-boxed">
<ul>
{navigationItems}
</ul>
<ul>{navigationItems}</ul>
</nav>
);
}

View File

@@ -1,4 +1,6 @@
// @flow
import queryString from "query-string";
export const contextPath = window.ctxPath || "";
export function withContextPath(path: string) {
@@ -27,3 +29,7 @@ export function getPageFromMatch(match: any) {
}
return page;
}
export function getQueryStringFromLocation(location: any) {
return location.search ? queryString.parse(location.search).q : undefined;
}

View File

@@ -1,5 +1,5 @@
// @flow
import {concat, getPageFromMatch, withEndingSlash} from "./urls";
import { concat, getPageFromMatch, getQueryStringFromLocation, withEndingSlash } from "./urls";
describe("tests for withEndingSlash", () => {
@@ -47,3 +47,28 @@ describe("tests for getPageFromMatch", () => {
expect(getPageFromMatch(match)).toBe(42);
});
});
describe("tests for getQueryStringFromLocation", () => {
function createLocation(search: string) {
return {
search
};
}
it("should return the query string", () => {
const location = createLocation("?q=abc");
expect(getQueryStringFromLocation(location)).toBe("abc");
});
it("should return query string from multiple parameters", () => {
const location = createLocation("?x=a&y=b&q=abc&z=c");
expect(getQueryStringFromLocation(location)).toBe("abc");
});
it("should return undefined if q is not available", () => {
const location = createLocation("?x=a&y=b&z=c");
expect(getQueryStringFromLocation(location)).toBeUndefined();
});
});

View File

@@ -693,9 +693,9 @@
version "0.0.2"
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
"@scm-manager/ui-bundler@^0.0.26":
version "0.0.26"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66"
"@scm-manager/ui-bundler@^0.0.28":
version "0.0.28"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e"
dependencies:
"@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0"
@@ -6530,6 +6530,14 @@ qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
query-string@5:
version "5.1.1"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb"
dependencies:
decode-uri-component "^0.2.0"
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
querystring-es3@~0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -7631,6 +7639,10 @@ stream-throttle@^0.1.3:
commander "^2.2.0"
limiter "^1.0.5"
strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
string-length@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"

View File

@@ -14,7 +14,7 @@
"check": "flow check"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.26"
"@scm-manager/ui-bundler": "^0.0.28"
},
"browserify": {
"transform": [

View File

@@ -707,9 +707,9 @@
version "0.0.2"
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
"@scm-manager/ui-bundler@^0.0.26":
version "0.0.26"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66"
"@scm-manager/ui-bundler@^0.0.28":
version "0.0.28"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e"
dependencies:
"@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0"

View File

@@ -54,7 +54,7 @@
"pre-commit": "jest && flow && eslint src"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.27",
"@scm-manager/ui-bundler": "^0.0.28",
"concat": "^1.0.3",
"copyfiles": "^2.0.0",
"enzyme": "^3.3.0",

View File

@@ -39,6 +39,7 @@
"groups": "Gruppen",
"config": "Einstellungen"
},
"filterEntries": "Einträge filtern",
"paginator": {
"next": "Weiter",
"previous": "Zurück"

View File

@@ -39,7 +39,7 @@
"setPermissionsNavLink": "Berechtigungen"
}
},
"addUser": {
"createUser": {
"title": "Benutzer erstellen",
"subtitle": "Erstellen eines neuen Benutzers"
},

View File

@@ -39,6 +39,7 @@
"groups": "Groups",
"config": "Configuration"
},
"filterEntries": "filter entries",
"paginator": {
"next": "Next",
"previous": "Previous"

View File

@@ -39,7 +39,7 @@
"setPermissionsNavLink": "Permissions"
}
},
"addUser": {
"createUser": {
"title": "Create User",
"subtitle": "Create a new user"
},

View File

@@ -2,24 +2,24 @@
import React from "react";
import { Redirect, Route, Switch, withRouter } from "react-router-dom";
import type {Links} from "@scm-manager/ui-types";
import type { Links } from "@scm-manager/ui-types";
import Overview from "../repos/containers/Overview";
import Users from "../users/containers/Users";
import Login from "../containers/Login";
import Logout from "../containers/Logout";
import {ProtectedRoute} from "@scm-manager/ui-components";
import {binder, ExtensionPoint } from "@scm-manager/ui-extensions";
import { ProtectedRoute } from "@scm-manager/ui-components";
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
import AddUser from "../users/containers/AddUser";
import CreateUser from "../users/containers/CreateUser";
import SingleUser from "../users/containers/SingleUser";
import RepositoryRoot from "../repos/containers/RepositoryRoot";
import Create from "../repos/containers/Create";
import Groups from "../groups/containers/Groups";
import SingleGroup from "../groups/containers/SingleGroup";
import AddGroup from "../groups/containers/AddGroup";
import CreateGroup from "../groups/containers/CreateGroup";
import Config from "../config/containers/Config";
import Profile from "./Profile";
@@ -33,14 +33,14 @@ class Main extends React.Component<Props> {
render() {
const { authenticated, links } = this.props;
const redirectUrlFactory = binder.getExtension("main.redirect", this.props);
let url ="/repos";
if (redirectUrlFactory){
let url = "/repos";
if (redirectUrlFactory) {
url = redirectUrlFactory(this.props);
}
return (
<div className="main">
<Switch>
<Redirect exact from="/" to={url}/>
<Redirect exact from="/" to={url} />
<Route exact path="/login" component={Login} />
<Route path="/logout" component={Logout} />
<ProtectedRoute
@@ -74,8 +74,8 @@ class Main extends React.Component<Props> {
/>
<ProtectedRoute
authenticated={authenticated}
path="/users/add"
component={AddUser}
path="/users/create"
component={CreateUser}
/>
<ProtectedRoute
exact
@@ -102,8 +102,8 @@ class Main extends React.Component<Props> {
/>
<ProtectedRoute
authenticated={authenticated}
path="/groups/add"
component={AddGroup}
path="/groups/create"
component={CreateGroup}
/>
<ProtectedRoute
exact
@@ -125,7 +125,7 @@ class Main extends React.Component<Props> {
<ExtensionPoint
name="main.route"
renderAll={true}
props={{authenticated, links}}
props={{ authenticated, links }}
/>
</Switch>
</div>

View File

@@ -40,7 +40,8 @@ class GroupForm extends React.Component<Props, State> {
},
_links: {},
members: [],
type: ""
type: "",
external: false
},
nameValidationError: false
};

View File

@@ -1,19 +0,0 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import { CreateButton } from "@scm-manager/ui-components";
type Props = {
t: string => string
};
class CreateGroupButton extends React.Component<Props> {
render() {
const { t } = this.props;
return (
<CreateButton label={t("create-group-button.label")} link="/groups/add" />
);
}
}
export default translate("groups")(CreateGroupButton);

View File

@@ -31,7 +31,7 @@ type Props = {
type State = {};
class AddGroup extends React.Component<Props, State> {
class CreateGroup extends React.Component<Props, State> {
componentDidMount() {
this.props.resetForm();
}
@@ -104,4 +104,4 @@ const mapStateToProps = state => {
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("groups")(AddGroup));
)(translate("groups")(CreateGroup));

View File

@@ -2,27 +2,26 @@
import React from "react";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import type { Group, PagedCollection } from "@scm-manager/ui-types";
import type { History } from "history";
import {
Page,
PageActions,
Button,
Notification,
Paginator
} from "@scm-manager/ui-components";
import { GroupTable } from "./../components/table";
import CreateGroupButton from "../components/buttons/CreateGroupButton";
import type { Group, PagedCollection } from "@scm-manager/ui-types";
import {
fetchGroupsByPage,
fetchGroupsByLink,
getGroupsFromState,
isFetchGroupsPending,
getFetchGroupsFailure,
isPermittedToCreateGroups,
selectListAsCollection
} from "../modules/groups";
import {
Page,
PageActions,
OverviewPageActions,
Notification,
LinkPaginator,
urls,
CreateButton
} from "@scm-manager/ui-components";
import { GroupTable } from "./../components/table";
import { getGroupsLink } from "../../modules/indexResource";
type Props = {
@@ -37,37 +36,45 @@ type Props = {
// context objects
t: string => string,
history: History,
location: any,
// dispatch functions
fetchGroupsByPage: (link: string, page: number) => void,
fetchGroupsByLink: (link: string) => void
fetchGroupsByPage: (link: string, page: number, filter?: string) => void
};
class Groups extends React.Component<Props> {
componentDidMount() {
this.props.fetchGroupsByPage(this.props.groupLink, this.props.page);
const { fetchGroupsByPage, groupLink, page, location } = this.props;
fetchGroupsByPage(
groupLink,
page,
urls.getQueryStringFromLocation(location)
);
}
onPageChange = (link: string) => {
this.props.fetchGroupsByLink(link);
};
/**
* reflect page transitions in the uri
*/
componentDidUpdate = (prevProps: Props) => {
const { page, list } = this.props;
if (list.page >= 0) {
// backend starts paging by 0
const {
loading,
list,
page,
groupLink,
location,
fetchGroupsByPage
} = this.props;
if (list && page && !loading) {
const statePage: number = list.page + 1;
if (page !== statePage) {
this.props.history.push(`/groups/${statePage}`);
if (page !== statePage || prevProps.location.search !== location.search) {
fetchGroupsByPage(
groupLink,
page,
urls.getQueryStringFromLocation(location)
);
}
}
};
render() {
const { groups, loading, error, t } = this.props;
const { groups, loading, error, canAddGroups, t } = this.props;
return (
<Page
title={t("groups.title")}
@@ -77,74 +84,56 @@ class Groups extends React.Component<Props> {
>
{this.renderGroupTable()}
{this.renderCreateButton()}
{this.renderPageActionCreateButton()}
<PageActions>
<OverviewPageActions
showCreateButton={canAddGroups}
link="groups"
label={t("create-group-button.label")}
/>
</PageActions>
</Page>
);
}
renderGroupTable() {
const { groups, t } = this.props;
const { groups, list, page, location, t } = this.props;
if (groups && groups.length > 0) {
return (
<>
<GroupTable groups={groups} />
{this.renderPaginator()}
<LinkPaginator
collection={list}
page={page}
filter={urls.getQueryStringFromLocation(location)}
/>
</>
);
}
return <Notification type="info">{t("groups.noGroups")}</Notification>;
}
renderPaginator() {
const { list } = this.props;
if (list) {
return <Paginator collection={list} onPageChange={this.onPageChange} />;
}
return null;
}
renderCreateButton() {
if (this.props.canAddGroups) {
return <CreateGroupButton />;
}
return null;
}
renderPageActionCreateButton() {
if (this.props.canAddGroups) {
const { canAddGroups, t } = this.props;
if (canAddGroups) {
return (
<PageActions>
<Button
label={this.props.t("create-group-button.label")}
link="/groups/add"
color="primary"
<CreateButton
label={t("create-group-button.label")}
link="/groups/create"
/>
</PageActions>
);
}
return null;
}
}
const getPageFromProps = props => {
let page = props.match.params.page;
if (page) {
page = parseInt(page, 10);
} else {
page = 1;
}
return page;
};
const mapStateToProps = (state, ownProps) => {
const { match } = ownProps;
const groups = getGroupsFromState(state);
const loading = isFetchGroupsPending(state);
const error = getFetchGroupsFailure(state);
const page = getPageFromProps(ownProps);
const page = urls.getPageFromMatch(match);
const canAddGroups = isPermittedToCreateGroups(state);
const list = selectListAsCollection(state);
const groupLink = getGroupsLink(state);
return {
@@ -160,11 +149,8 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = dispatch => {
return {
fetchGroupsByPage: (link: string, page: number) => {
dispatch(fetchGroupsByPage(link, page));
},
fetchGroupsByLink: (link: string) => {
dispatch(fetchGroupsByLink(link));
fetchGroupsByPage: (link: string, page: number, filter?: string) => {
dispatch(fetchGroupsByPage(link, page, filter));
}
};
};

View File

@@ -40,9 +40,14 @@ export function fetchGroups(link: string) {
return fetchGroupsByLink(link);
}
export function fetchGroupsByPage(link: string, page: number) {
export function fetchGroupsByPage(link: string, page: number, filter?: string) {
// backend start counting by 0
return fetchGroupsByLink(link + "?page=" + (page - 1));
if (filter) {
return fetchGroupsByLink(
`${link}?page=${page - 1}&q=${decodeURIComponent(filter)}`
);
}
return fetchGroupsByLink(`${link}?page=${page - 1}`);
}
export function fetchGroupsByLink(link: string) {

View File

@@ -97,14 +97,14 @@ export const logoutPending = () => {
export const logoutSuccess = () => {
return {
type: LOGOUT_SUCCESS,
type: LOGOUT_SUCCESS
};
};
export const redirectAfterLogout = () => {
return {
type: LOGOUT_REDIRECT
}
};
};
export const logoutFailure = (error: Error) => {
@@ -277,4 +277,3 @@ export const getLogoutFailure = (state: Object) => {
export const isRedirecting = (state: Object) => {
return !!stateAuth(state).redirecting;
};

View File

@@ -15,7 +15,7 @@ import type {
import { isPending } from "../../../modules/pending";
import { getFailure } from "../../../modules/failure";
import memoizeOne from 'memoize-one';
import memoizeOne from "memoize-one";
export const FETCH_BRANCHES = "scm/repos/FETCH_BRANCHES";
export const FETCH_BRANCHES_PENDING = `${FETCH_BRANCHES}_${PENDING_SUFFIX}`;
@@ -111,9 +111,7 @@ export function createBranch(
// Selectors
function collectBranches(repoState) {
return repoState.list._embedded.branches.map(
name => repoState.byName[name]
);
return repoState.list._embedded.branches.map(name => repoState.byName[name]);
}
const memoizedBranchCollector = memoizeOne(collectBranches);
@@ -127,7 +125,12 @@ export function getBranches(state: Object, repository: Repository) {
export function getBranchCreateLink(state: Object, repository: Repository) {
const repoState = getRepoState(state, repository);
if (repoState && repoState.list && repoState.list._links && repoState.list._links.create) {
if (
repoState &&
repoState.list &&
repoState.list._links &&
repoState.list._links.create
) {
return repoState.list._links.create.href;
}
}

View File

@@ -1,71 +1,79 @@
// @flow
import React from "react";
import type { RepositoryCollection } from "@scm-manager/ui-types";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import type { History } from "history";
import { withRouter } from "react-router-dom";
import type { RepositoryCollection } from "@scm-manager/ui-types";
import {
fetchRepos,
fetchReposByLink,
fetchReposByPage,
getFetchReposFailure,
getRepositoryCollection,
isAbleToCreateRepos,
isFetchReposPending
} from "../modules/repos";
import { translate } from "react-i18next";
import {
Page,
PageActions,
Button,
OverviewPageActions,
CreateButton,
Notification,
Paginator
LinkPaginator,
urls
} from "@scm-manager/ui-components";
import RepositoryList from "../components/list";
import { withRouter } from "react-router-dom";
import type { History } from "history";
import { getRepositoriesLink } from "../../modules/indexResource";
type Props = {
page: number,
collection: RepositoryCollection,
loading: boolean,
error: Error,
showCreateButton: boolean,
collection: RepositoryCollection,
page: number,
reposLink: string,
// dispatched functions
fetchRepos: string => void,
fetchReposByPage: (string, number) => void,
fetchReposByLink: string => void,
// context props
t: string => string,
history: History
history: History,
location: any,
// dispatched functions
fetchReposByPage: (link: string, page: number, filter?: string) => void
};
class Overview extends React.Component<Props> {
componentDidMount() {
this.props.fetchReposByPage(this.props.reposLink, this.props.page);
const { fetchReposByPage, reposLink, page, location } = this.props;
fetchReposByPage(
reposLink,
page,
urls.getQueryStringFromLocation(location)
);
}
/**
* reflect page transitions in the uri
*/
componentDidUpdate() {
const { page, collection } = this.props;
if (collection) {
// backend starts paging by 0
componentDidUpdate = (prevProps: Props) => {
const {
loading,
collection,
page,
reposLink,
location,
fetchReposByPage
} = this.props;
if (collection && page && !loading) {
const statePage: number = collection.page + 1;
if (page !== statePage) {
this.props.history.push(`/repos/${statePage}`);
}
if (page !== statePage || prevProps.location.search !== location.search) {
fetchReposByPage(
reposLink,
page,
urls.getQueryStringFromLocation(location)
);
}
}
};
render() {
const { error, loading, t } = this.props;
const { error, loading, showCreateButton, t } = this.props;
return (
<Page
title={t("overview.title")}
@@ -74,19 +82,29 @@ class Overview extends React.Component<Props> {
error={error}
>
{this.renderOverview()}
{this.renderPageActionCreateButton()}
<PageActions>
<OverviewPageActions
showCreateButton={showCreateButton}
link="repos"
label={t("overview.createButton")}
/>
</PageActions>
</Page>
);
}
renderRepositoryList() {
const { collection, fetchReposByLink, t } = this.props;
const { collection, page, location, t } = this.props;
if (collection._embedded && collection._embedded.repositories.length > 0) {
return (
<>
<RepositoryList repositories={collection._embedded.repositories} />
<Paginator collection={collection} onPageChange={fetchReposByLink} />
<LinkPaginator
collection={collection}
page={page}
filter={urls.getQueryStringFromLocation(location)}
/>
</>
);
}
@@ -99,10 +117,10 @@ class Overview extends React.Component<Props> {
const { collection } = this.props;
if (collection) {
return (
<div>
<>
{this.renderRepositoryList()}
{this.renderCreateButton()}
</div>
</>
);
}
return null;
@@ -117,61 +135,30 @@ class Overview extends React.Component<Props> {
}
return null;
}
renderPageActionCreateButton() {
const { showCreateButton, t } = this.props;
if (showCreateButton) {
return (
<PageActions>
<Button
label={t("overview.createButton")}
link="/repos/create"
color="primary"
/>
</PageActions>
);
}
return null;
}
}
const getPageFromProps = props => {
let page = props.match.params.page;
if (page) {
page = parseInt(page, 10);
} else {
page = 1;
}
return page;
};
const mapStateToProps = (state, ownProps) => {
const page = getPageFromProps(ownProps);
const { match } = ownProps;
const collection = getRepositoryCollection(state);
const loading = isFetchReposPending(state);
const error = getFetchReposFailure(state);
const page = urls.getPageFromMatch(match);
const showCreateButton = isAbleToCreateRepos(state);
const reposLink = getRepositoriesLink(state);
return {
reposLink,
page,
collection,
loading,
error,
showCreateButton
page,
showCreateButton,
reposLink
};
};
const mapDispatchToProps = dispatch => {
return {
fetchRepos: (link: string) => {
dispatch(fetchRepos(link));
},
fetchReposByPage: (link: string, page: number) => {
dispatch(fetchReposByPage(link, page));
},
fetchReposByLink: (link: string) => {
dispatch(fetchReposByLink(link));
fetchReposByPage: (link: string, page: number, filter?: string) => {
dispatch(fetchReposByPage(link, page, filter));
}
};
};

View File

@@ -46,7 +46,12 @@ export function fetchRepos(link: string) {
return fetchReposByLink(link);
}
export function fetchReposByPage(link: string, page: number) {
export function fetchReposByPage(link: string, page: number, filter?: string) {
if (filter) {
return fetchReposByLink(
`${link}?page=${page - 1}&q=${decodeURIComponent(filter)}`
);
}
return fetchReposByLink(`${link}?page=${page - 1}`);
}

View File

@@ -28,7 +28,7 @@ type Props = {
history: History
};
class AddUser extends React.Component<Props> {
class CreateUser extends React.Component<Props> {
componentDidMount() {
this.props.resetForm();
}
@@ -49,8 +49,8 @@ class AddUser extends React.Component<Props> {
return (
<Page
title={t("addUser.title")}
subtitle={t("addUser.subtitle")}
title={t("createUser.title")}
subtitle={t("createUser.subtitle")}
error={error}
showContentOnError={true}
>
@@ -88,4 +88,4 @@ const mapStateToProps = (state, ownProps) => {
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("users")(AddUser));
)(translate("users")(CreateUser));

View File

@@ -1,29 +1,27 @@
// @flow
import React from "react";
import type { History } from "history";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import type { History } from "history";
import type { User, PagedCollection } from "@scm-manager/ui-types";
import {
fetchUsersByPage,
fetchUsersByLink,
getUsersFromState,
selectListAsCollection,
isPermittedToCreateUsers,
isFetchUsersPending,
getFetchUsersFailure
} from "../modules/users";
import {
Page,
PageActions,
Button,
CreateButton,
Paginator,
Notification
OverviewPageActions,
Notification,
LinkPaginator,
urls,
CreateButton
} from "@scm-manager/ui-components";
import { UserTable } from "./../components/table";
import type { User, PagedCollection } from "@scm-manager/ui-types";
import { getUsersLink } from "../../modules/indexResource";
type Props = {
@@ -38,37 +36,45 @@ type Props = {
// context objects
t: string => string,
history: History,
location: any,
// dispatch functions
fetchUsersByPage: (link: string, page: number) => void,
fetchUsersByLink: (link: string) => void
fetchUsersByPage: (link: string, page: number, filter?: string) => void
};
class Users extends React.Component<Props> {
componentDidMount() {
this.props.fetchUsersByPage(this.props.usersLink, this.props.page);
const { fetchUsersByPage, usersLink, page, location } = this.props;
fetchUsersByPage(
usersLink,
page,
urls.getQueryStringFromLocation(location)
);
}
onPageChange = (link: string) => {
this.props.fetchUsersByLink(link);
componentDidUpdate = (prevProps: Props) => {
const {
loading,
list,
page,
usersLink,
location,
fetchUsersByPage
} = this.props;
if (list && page && !loading) {
const statePage: number = list.page + 1;
if (page !== statePage || prevProps.location.search !== location.search) {
fetchUsersByPage(
usersLink,
page,
urls.getQueryStringFromLocation(location)
);
}
}
};
/**
* reflect page transitions in the uri
*/
componentDidUpdate() {
const { page, list } = this.props;
if (list && (list.page || list.page === 0)) {
// backend starts paging by 0
const statePage: number = list.page + 1;
if (page !== statePage) {
this.props.history.push(`/users/${statePage}`);
}
}
}
render() {
const { users, loading, error, t } = this.props;
const { users, loading, error, canAddUsers, t } = this.props;
return (
<Page
title={t("users.title")}
@@ -78,79 +84,54 @@ class Users extends React.Component<Props> {
>
{this.renderUserTable()}
{this.renderCreateButton()}
{this.renderPageActionCreateButton()}
<PageActions>
<OverviewPageActions
showCreateButton={canAddUsers}
link="users"
label={t("users.createButton")}
/>
</PageActions>
</Page>
);
}
renderUserTable() {
const { users, t } = this.props;
const { users, list, page, location, t } = this.props;
if (users && users.length > 0) {
return (
<>
<UserTable users={users} />
{this.renderPaginator()}
<LinkPaginator
collection={list}
page={page}
filter={urls.getQueryStringFromLocation(location)}
/>
</>
);
}
return <Notification type="info">{t("users.noUsers")}</Notification>;
}
renderPaginator() {
const { list } = this.props;
if (list) {
return <Paginator collection={list} onPageChange={this.onPageChange} />;
renderCreateButton() {
const { canAddUsers, t } = this.props;
if (canAddUsers) {
return (
<CreateButton label={t("users.createButton")} link="/users/create" />
);
}
return null;
}
renderCreateButton() {
const { t } = this.props;
if (this.props.canAddUsers) {
return <CreateButton label={t("users.createButton")} link="/users/add" />;
} else {
return;
}
}
renderPageActionCreateButton() {
const { t } = this.props;
if (this.props.canAddUsers) {
return (
<PageActions>
<Button
label={t("users.createButton")}
link="/users/add"
color="primary"
/>
</PageActions>
);
} else {
return;
}
}
}
const getPageFromProps = props => {
let page = props.match.params.page;
if (page) {
page = parseInt(page, 10);
} else {
page = 1;
}
return page;
};
const mapStateToProps = (state, ownProps) => {
const { match } = ownProps;
const users = getUsersFromState(state);
const loading = isFetchUsersPending(state);
const error = getFetchUsersFailure(state);
const usersLink = getUsersLink(state);
const page = getPageFromProps(ownProps);
const page = urls.getPageFromMatch(match);
const canAddUsers = isPermittedToCreateUsers(state);
const list = selectListAsCollection(state);
const usersLink = getUsersLink(state);
return {
users,
@@ -165,11 +146,8 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = dispatch => {
return {
fetchUsersByPage: (link: string, page: number) => {
dispatch(fetchUsersByPage(link, page));
},
fetchUsersByLink: (link: string) => {
dispatch(fetchUsersByLink(link));
fetchUsersByPage: (link: string, page: number, filter?: string) => {
dispatch(fetchUsersByPage(link, page, filter));
}
};
};

View File

@@ -43,9 +43,14 @@ export function fetchUsers(link: string) {
return fetchUsersByLink(link);
}
export function fetchUsersByPage(link: string, page: number) {
export function fetchUsersByPage(link: string, page: number, filter?: string) {
// backend start counting by 0
return fetchUsersByLink(link + "?page=" + (page - 1));
if (filter) {
return fetchUsersByLink(
`${link}?page=${page - 1}&q=${decodeURIComponent(filter)}`
);
}
return fetchUsersByLink(`${link}?page=${page - 1}`);
}
export function fetchUsersByLink(link: string) {
@@ -153,9 +158,7 @@ export function createUser(link: string, user: User, callback?: () => void) {
callback();
}
})
.catch(error =>
dispatch(createUserFailure(error))
);
.catch(error => dispatch(createUserFailure(error)));
};
}

View File

@@ -49,14 +49,23 @@ hr.header-with-actions {
display: none;
}
}
.is-mobile-create-button-spacing {
.is-mobile-action-spacing {
@media screen and (max-width: 768px) {
display: flow-root !important;
.input-field {
padding: 0;
margin: 0 0 1.25rem 0 !important;
width: 100%;
}
.input-button {
border: 2px solid #e9f7fd;
padding: 1em 1em;
margin-top: 0 !important;
width: 100%;
text-align: center !important;
}
}
}
.footer {

View File

@@ -698,9 +698,9 @@
version "0.0.2"
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
"@scm-manager/ui-bundler@^0.0.27":
version "0.0.27"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.27.tgz#3ed2c7826780b9a1a9ea90464332640cfb5d54b5"
"@scm-manager/ui-bundler@^0.0.28":
version "0.0.28"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e"
dependencies:
"@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0"

View File

@@ -52,12 +52,7 @@
]]>
</description>
<api-classes>
<exclude pattern="sonia.scm.debug.DebugResource" />
<exclude pattern="sonia.scm.api.rest.resources.ConfigurationResource" />
<exclude pattern="sonia.scm.api.rest.resources.SupportResource" />
<exclude pattern="sonia.scm.api.rest.resources.RepositoryRootResource" />
</api-classes>
<api-classes/>
<modules>

View File

@@ -1,581 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.common.net.UrlEscapers;
import org.apache.shiro.authz.AuthorizationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.LastModifiedAware;
import sonia.scm.Manager;
import sonia.scm.ModelObject;
import sonia.scm.PageResult;
import sonia.scm.api.rest.RestExceptionResult;
import sonia.scm.util.AssertUtil;
import sonia.scm.util.Comparables;
import sonia.scm.util.Util;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
//~--- JDK imports ------------------------------------------------------------
public abstract class AbstractManagerResource<T extends ModelObject> {
/** the logger for AbstractManagerResource */
private static final Logger logger =
LoggerFactory.getLogger(AbstractManagerResource.class);
protected final Manager<T> manager;
private final Class<T> type;
protected int cacheMaxAge = 0;
protected boolean disableCache = false;
public AbstractManagerResource(Manager<T> manager, Class<T> type) {
this.manager = manager;
this.type = type;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param items
*
* @return
*/
protected abstract GenericEntity<Collection<T>> createGenericEntity(
Collection<T> items);
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param item
*
* @return
*/
protected abstract String getId(T item);
/**
* Method description
*
*
* @return
*/
protected abstract String getPathPart();
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
*
* @param uriInfo
* @param item
*
* @return
*/
public Response create(UriInfo uriInfo, T item)
{
preCreate(item);
Response response;
try
{
manager.create(item);
String id = getId(item);
response = Response.created(location(uriInfo, id)).build();
}
catch (AuthorizationException ex)
{
logger.warn("create is not allowd", ex);
response = Response.status(Status.FORBIDDEN).build();
}
catch (Exception ex)
{
logger.error("error during create", ex);
response = createErrorResponse(ex);
}
return response;
}
@VisibleForTesting
URI location(UriInfo uriInfo, String id) {
String escaped = UrlEscapers.urlPathSegmentEscaper().escape(id);
return uriInfo.getAbsolutePath().resolve(getPathPart().concat("/").concat(escaped));
}
/**
* Method description
*
*
* @param name
*
* @return
*/
public Response delete(String name)
{
Response response = null;
T item = manager.get(name);
if (item != null)
{
preDelete(item);
try
{
manager.delete(item);
response = Response.noContent().build();
}
catch (AuthorizationException ex)
{
logger.warn("delete not allowd", ex);
response = Response.status(Response.Status.FORBIDDEN).build();
}
catch (Exception ex)
{
logger.error("error during delete", ex);
response = createErrorResponse(ex);
}
}
return response;
}
/**
* Method description
*
*
*
*
* @param name
* @param item
*
*
* @return
*/
public Response update(String name, T item)
{
Response response = null;
preUpdate(item);
try
{
manager.modify(item);
response = Response.noContent().build();
}
catch (AuthorizationException ex)
{
logger.warn("update not allowed", ex);
response = Response.status(Response.Status.FORBIDDEN).build();
}
catch (Exception ex)
{
logger.error("error during update", ex);
response = createErrorResponse(ex);
}
return response;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
*
* @param request
* @param id
*
* @return
*/
public Response get(Request request, String id)
{
Response response;
T item = manager.get(id);
if (item != null)
{
prepareForReturn(item);
if (disableCache)
{
response = Response.ok(item).build();
}
else
{
response = createCacheResponse(request, item, item);
}
}
else
{
response = Response.status(Response.Status.NOT_FOUND).build();
}
return response;
}
/**
* Method description
*
*
*
* @param request
* @param start
* @param limit
* @param sortby
* @param desc
* @return
*/
public Response getAll(Request request, int start, int limit, String sortby,
boolean desc)
{
Collection<T> items = fetchItems(sortby, desc, start, limit);
if (Util.isNotEmpty(items))
{
items = prepareForReturn(items);
}
Response response = null;
Object entity = createGenericEntity(items);
if (disableCache)
{
response = Response.ok(entity).build();
}
else
{
response = createCacheResponse(request, manager, items, entity);
}
return response;
}
/**
* Method description
*
*
* @return
*/
public int getCacheMaxAge()
{
return cacheMaxAge;
}
/**
* Method description
*
*
* @return
*/
public boolean isDisableCache()
{
return disableCache;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param cacheMaxAge
*/
public void setCacheMaxAge(int cacheMaxAge)
{
this.cacheMaxAge = cacheMaxAge;
}
/**
* Method description
*
*
* @param disableCache
*/
public void setDisableCache(boolean disableCache)
{
this.disableCache = disableCache;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param throwable
*
* @return
*/
protected Response createErrorResponse(Throwable throwable)
{
return createErrorResponse(Status.INTERNAL_SERVER_ERROR,
throwable.getMessage(), throwable);
}
/**
* Method description
*
*
* @param status
* @param message
* @param throwable
*
* @return
*/
protected Response createErrorResponse(Status status, String message,
Throwable throwable)
{
return Response.status(status).entity(new RestExceptionResult(message,
throwable)).build();
}
/**
* Method description
*
*
* @param item
*/
protected void preCreate(T item) {}
/**
* Method description
*
*
* @param item
*/
protected void preDelete(T item) {}
/**
* Method description
*
*
* @param item
*/
protected void preUpdate(T item) {}
/**
* Method description
*
*
* @param item
*
* @return
*/
protected T prepareForReturn(T item)
{
return item;
}
/**
* Method description
*
*
* @param items
*
* @return
*/
protected Collection<T> prepareForReturn(Collection<T> items)
{
return items;
}
/**
* Method description
*
*
* @param rb
*/
private void addCacheControl(Response.ResponseBuilder rb)
{
CacheControl cc = new CacheControl();
cc.setMaxAge(cacheMaxAge);
rb.cacheControl(cc);
}
/**
* Method description
*
*
* @param request
* @param timeItem
* @param item
* @param <I>
*
* @return
*/
private <I> Response createCacheResponse(Request request,
LastModifiedAware timeItem, I item)
{
return createCacheResponse(request, timeItem, item, item);
}
/**
* Method description
*
*
* @param request
* @param timeItem
* @param entityItem
* @param item
* @param <I>
*
* @return
*/
private <I> Response createCacheResponse(Request request,
LastModifiedAware timeItem, Object entityItem, I item)
{
Response.ResponseBuilder builder = null;
Date lastModified = getLastModified(timeItem);
EntityTag e = new EntityTag(Integer.toString(entityItem.hashCode()));
if (lastModified != null)
{
builder = request.evaluatePreconditions(lastModified, e);
}
else
{
builder = request.evaluatePreconditions(e);
}
if (builder == null)
{
builder = Response.ok(item).tag(e).lastModified(lastModified);
}
addCacheControl(builder);
return builder.build();
}
private Comparator<T> createComparator(String sortBy, boolean desc) {
Comparator<T> comparator = Comparables.comparator(type, sortBy);
if (desc) {
comparator = comparator.reversed();
}
return comparator;
}
private Collection<T> fetchItems(String sortBy, boolean desc, int start,
int limit)
{
AssertUtil.assertPositive(start);
Collection<T> items = null;
if (limit > 0)
{
if (Util.isEmpty(sortBy))
{
// replace with something useful
sortBy = "id";
}
items = manager.getAll(createComparator(sortBy, desc), start, limit);
}
else if (Util.isNotEmpty(sortBy))
{
items = manager.getAll(createComparator(sortBy, desc));
}
else
{
items = manager.getAll();
}
return items;
}
protected PageResult<T> fetchPage(String sortBy, boolean desc, int pageNumber,
int pageSize) {
AssertUtil.assertPositive(pageNumber);
AssertUtil.assertPositive(pageSize);
if (Util.isEmpty(sortBy)) {
// replace with something useful
sortBy = "id";
}
return manager.getPage(createComparator(sortBy, desc), pageNumber, pageSize);
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @param item
*
* @return
*/
private Date getLastModified(LastModifiedAware item)
{
Date lastModified = null;
Long l = item.getLastModified();
if (l != null)
{
lastModified = new Date(l);
}
return lastModified;
}
}

View File

@@ -1,37 +0,0 @@
package sonia.scm.api.rest.resources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.api.CatCommandBuilder;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.util.IOUtil;
import javax.ws.rs.core.StreamingOutput;
import java.io.IOException;
import java.io.OutputStream;
public class BrowserStreamingOutput implements StreamingOutput {
private static final Logger logger =
LoggerFactory.getLogger(BrowserStreamingOutput.class);
private final CatCommandBuilder builder;
private final String path;
private final RepositoryService repositoryService;
public BrowserStreamingOutput(RepositoryService repositoryService,
CatCommandBuilder builder, String path) {
this.repositoryService = repositoryService;
this.builder = builder;
this.path = path;
}
@Override
public void write(OutputStream output) throws IOException {
try {
builder.retriveContent(output, path);
} finally {
IOUtil.close(repositoryService);
}
}
}

View File

@@ -1,173 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.PasswordService;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.api.rest.RestActionResult;
import sonia.scm.security.Role;
import sonia.scm.security.ScmSecurityException;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.util.AssertUtil;
import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
//~--- JDK imports ------------------------------------------------------------
/**
* Resource to change the password of the authenticated user.
*
* @author Sebastian Sdorra
*/
@Path("action/change-password")
public class ChangePasswordResource
{
/** the logger for ChangePasswordResource */
private static final Logger logger =
LoggerFactory.getLogger(ChangePasswordResource.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param userManager
* @param encryptionHandler
*/
@Inject
public ChangePasswordResource(UserManager userManager,
PasswordService encryptionHandler)
{
this.userManager = userManager;
this.passwordService = encryptionHandler;
}
//~--- methods --------------------------------------------------------------
/**
* Changes the password of the current user.
*
* @param oldPassword old password of the current user
* @param newPassword new password for the current user
*/
@POST
@TypeHint(RestActionResult.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, the old password is not correct"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response changePassword(@FormParam("old-password") String oldPassword, @FormParam("new-password") String newPassword) {
AssertUtil.assertIsNotEmpty(oldPassword);
AssertUtil.assertIsNotEmpty(newPassword);
int length = newPassword.length();
if ((length < 6) || (length > 32))
{
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
Response response = null;
Subject subject = SecurityUtils.getSubject();
if (!subject.hasRole(Role.USER))
{
throw new ScmSecurityException("user is not authenticated");
}
User currentUser = subject.getPrincipals().oneByType(User.class);
if (logger.isInfoEnabled())
{
logger.info("password change for user {}", currentUser.getName());
}
// Only account of the default type can change their password
if (currentUser.getType().equals(userManager.getDefaultType()))
{
User dbUser = userManager.get(currentUser.getName());
if (passwordService.passwordsMatch(oldPassword, dbUser.getPassword()))
{
dbUser.setPassword(passwordService.encryptPassword(newPassword));
userManager.modify(dbUser);
response = Response.ok(new RestActionResult(true)).build();
}
else
{
response = Response.status(Response.Status.BAD_REQUEST).build();
}
}
else
{
//J-
logger.error(
"Only account of the default type ({}) can change their password",
userManager.getDefaultType()
);
//J+
response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
return response;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final PasswordService passwordService;
/** Field description */
private final UserManager userManager;
}

View File

@@ -1,109 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.api.DiffCommandBuilder;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.util.IOUtil;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.StreamingOutput;
import java.io.IOException;
import java.io.OutputStream;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public class DiffStreamingOutput implements StreamingOutput
{
/** the logger for DiffStreamingOutput */
private static final Logger logger =
LoggerFactory.getLogger(DiffStreamingOutput.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
*
* @param repositoryService
* @param builder
*/
public DiffStreamingOutput(RepositoryService repositoryService,
DiffCommandBuilder builder)
{
this.repositoryService = repositoryService;
this.builder = builder;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param output
*
* @throws IOException
* @throws WebApplicationException
*/
@Override
public void write(OutputStream output) throws IOException {
try
{
builder.retrieveContent(output);
}
finally
{
IOUtil.close(repositoryService);
}
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final DiffCommandBuilder builder;
/** Field description */
private final RepositoryService repositoryService;
}

View File

@@ -1,213 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.api.rest.resources;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.inject.Inject;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryTypePredicate;
import sonia.scm.template.Viewable;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
*
* @author Sebastian Sdorra
*/
@Path("help/repository-root/{type}.html")
public class RepositoryRootResource
{
private static final String TEMPLATE = "/templates/repository-root.mustache";
private final RepositoryManager repositoryManager;
/**
* Constructs ...
*
* @param repositoryManager
*/
@Inject
public RepositoryRootResource(RepositoryManager repositoryManager)
{
this.repositoryManager = repositoryManager;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
*
* @param request
* @param type
*
* @return
*
* @throws IOException
*/
@GET
@Produces(MediaType.TEXT_HTML)
public Viewable renderRepositoriesRoot(@Context HttpServletRequest request, @PathParam("type") final String type)
{
//J-
Collection<RepositoryTemplateElement> unsortedRepositories =
Collections2.transform(
Collections2.filter(
repositoryManager.getAll(), new RepositoryTypePredicate(type))
, new RepositoryTransformFunction()
);
List<RepositoryTemplateElement> repositories = Ordering.from(
new RepositoryTemplateElementComparator()
).sortedCopy(unsortedRepositories);
//J+
Map<String, Object> environment = Maps.newHashMap();
environment.put("repositories", repositories);
return new Viewable(TEMPLATE, environment);
}
//~--- inner classes --------------------------------------------------------
/**
* Class description
*
*
* @version Enter version here..., 12/05/28
* @author Enter your name here...
*/
public static class RepositoryTemplateElement
{
public RepositoryTemplateElement(Repository repository)
{
this.repository = repository;
}
//~--- get methods --------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public String getName()
{
return repository.getName();
}
/**
* Method description
*
*
* @return
*/
public Repository getRepository()
{
return repository;
}
//~--- fields -------------------------------------------------------------
/** Field description */
private Repository repository;
}
/**
* Class description
*
*
* @version Enter version here..., 12/05/29
* @author Enter your name here...
*/
private static class RepositoryTemplateElementComparator
implements Comparator<RepositoryTemplateElement>
{
/**
* Method description
*
*
* @param left
* @param right
*
* @return
*/
@Override
public int compare(RepositoryTemplateElement left,
RepositoryTemplateElement right)
{
return left.getName().compareTo(right.getName());
}
}
/**
* Class description
*
*
* @version Enter version here..., 12/05/28
* @author Enter your name here...
*/
private static class RepositoryTransformFunction
implements Function<Repository, RepositoryTemplateElement>
{
@Override
public RepositoryTemplateElement apply(Repository repository)
{
return new RepositoryTemplateElement(repository);
}
}
}

View File

@@ -1,216 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.github.legman.Subscribe;
import com.google.common.base.Function;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.group.Group;
import sonia.scm.group.GroupEvent;
import sonia.scm.group.GroupManager;
import sonia.scm.search.SearchHandler;
import sonia.scm.search.SearchResult;
import sonia.scm.search.SearchResults;
import sonia.scm.user.User;
import sonia.scm.user.UserEvent;
import sonia.scm.user.UserManager;
//~--- JDK imports ------------------------------------------------------------
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
/**
* RESTful Web Service Resource to search users and groups. This endpoint can be used to implement typeahead input
* fields for permissions.
*
* @author Sebastian Sdorra
*/
@Singleton
@Path("search")
public class SearchResource
{
/** Field description */
public static final String CACHE_GROUP = "sonia.cache.search.groups";
/** Field description */
public static final String CACHE_USER = "sonia.cache.search.users";
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param userManager
* @param groupManager
* @param cacheManager
*/
@Inject
public SearchResource(UserManager userManager, GroupManager groupManager,
CacheManager cacheManager)
{
// create user searchhandler
Cache<String, SearchResults> userCache = cacheManager.getCache(CACHE_USER);
this.userSearchHandler = new SearchHandler<User>(userCache, userManager);
// create group searchhandler
Cache<String, SearchResults> groupCache =
cacheManager.getCache(CACHE_GROUP);
this.groupSearchHandler = new SearchHandler<Group>(groupCache,
groupManager);
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param event
*/
@Subscribe
public void onEvent(UserEvent event)
{
if (event.getEventType().isPost())
{
userSearchHandler.clearCache();
}
}
/**
* Method description
*
*
* @param event
*/
@Subscribe
public void onEvent(GroupEvent event)
{
if (event.getEventType().isPost())
{
groupSearchHandler.clearCache();
}
}
/**
* Returns a list of groups found by the given search string.
*
* @param queryString the search string
*
* @return
*/
@GET
@Path("groups")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public SearchResults searchGroups(@QueryParam("query") String queryString)
{
return groupSearchHandler.search(queryString,
new Function<Group, SearchResult>()
{
@Override
public SearchResult apply(Group group)
{
String label = group.getName();
String description = group.getDescription();
if (description != null)
{
label = label.concat(" (").concat(description).concat(")");
}
return new SearchResult(group.getName(), label);
}
});
}
/**
* Returns a list of users found by the given search string.
*
* @param queryString the search string
*
* @return
*/
@GET
@Path("users")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public SearchResults searchUsers(@QueryParam("query") String queryString)
{
return userSearchHandler.search(queryString,
new Function<User, SearchResult>()
{
@Override
public SearchResult apply(User user)
{
StringBuilder label = new StringBuilder(user.getName());
label.append(" (").append(user.getDisplayName()).append(")");
return new SearchResult(user.getName(), label.toString());
}
});
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final SearchHandler<Group> groupSearchHandler;
/** Field description */
private final SearchHandler<User> userSearchHandler;
}

View File

@@ -4,13 +4,15 @@ import de.otto.edison.hal.HalRepresentation;
import sonia.scm.Manager;
import sonia.scm.ModelObject;
import sonia.scm.PageResult;
import sonia.scm.api.rest.resources.AbstractManagerResource;
import sonia.scm.util.AssertUtil;
import sonia.scm.util.Comparables;
import sonia.scm.util.Util;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.Collection;
import java.util.Comparator;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
@@ -27,21 +29,46 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
*/
@SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right?
class CollectionResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
DTO extends HalRepresentation> extends AbstractManagerResource<MODEL_OBJECT> {
DTO extends HalRepresentation>{
protected final Manager<MODEL_OBJECT> manager;
protected final Class<MODEL_OBJECT> type;
CollectionResourceManagerAdapter(Manager<MODEL_OBJECT> manager, Class<MODEL_OBJECT> type) {
super(manager, type);
this.manager = manager;
this.type = type;
}
/**
* Reads all model objects in a paged way, maps them using the given function and returns a corresponding http response.
* This handles all corner cases, eg. missing privileges.
*/
public Response getAll(int page, int pageSize, String sortBy, boolean desc, Function<PageResult<MODEL_OBJECT>, CollectionDto> mapToDto) {
PageResult<MODEL_OBJECT> pageResult = fetchPage(sortBy, desc, page, pageSize);
public Response getAll(int page, int pageSize, Predicate<MODEL_OBJECT> filter, String sortBy, boolean desc, Function<PageResult<MODEL_OBJECT>, CollectionDto> mapToDto) {
PageResult<MODEL_OBJECT> pageResult = fetchPage(filter, sortBy, desc, page, pageSize);
return Response.ok(mapToDto.apply(pageResult)).build();
}
private PageResult<MODEL_OBJECT> fetchPage(Predicate<MODEL_OBJECT> filter, String sortBy, boolean desc, int pageNumber,
int pageSize) {
AssertUtil.assertPositive(pageNumber);
AssertUtil.assertPositive(pageSize);
if (Util.isEmpty(sortBy)) {
// replace with something useful
sortBy = "id";
}
return manager.getPage(filter, createComparator(sortBy, desc), pageNumber, pageSize);
}
private Comparator<MODEL_OBJECT> createComparator(String sortBy, boolean desc) {
Comparator<MODEL_OBJECT> comparator = Comparables.comparator(type, sortBy);
if (desc) {
comparator = comparator.reversed();
}
return comparator;
}
/**
* Creates a model object for the given dto and returns a corresponding http response.
* This handles all corner cases, eg. no conflicts or missing privileges.
@@ -55,18 +82,7 @@ class CollectionResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
return Response.created(URI.create(uriCreator.apply(created))).build();
}
@Override
protected GenericEntity<Collection<MODEL_OBJECT>> createGenericEntity(Collection<MODEL_OBJECT> modelObjects) {
throw new UnsupportedOperationException();
}
@Override
protected String getId(MODEL_OBJECT item) {
return item.getId();
}
@Override
protected String getPathPart() {
throw new UnsupportedOperationException();
}
}

View File

@@ -7,6 +7,8 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.group.Group;
import sonia.scm.group.GroupManager;
import sonia.scm.search.SearchRequest;
import sonia.scm.search.SearchUtil;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
@@ -19,6 +21,9 @@ import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import java.util.function.Predicate;
import static com.google.common.base.Strings.isNullOrEmpty;
public class GroupCollectionResource {
@@ -63,8 +68,10 @@ public class GroupCollectionResource {
@DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize,
@QueryParam("sortBy") String sortBy,
@DefaultValue("false")
@QueryParam("desc") boolean desc) {
return adapter.getAll(page, pageSize, sortBy, desc,
@QueryParam("desc") boolean desc,
@DefaultValue("") @QueryParam("q") String search
) {
return adapter.getAll(page, pageSize, createSearchPredicate(search), sortBy, desc,
pageResult -> groupCollectionToDtoMapper.map(page, pageSize, pageResult));
}
@@ -90,4 +97,12 @@ public class GroupCollectionResource {
() -> dtoToGroupMapper.map(group),
g -> resourceLinks.group().self(g.getName()));
}
private Predicate<Group> createSearchPredicate(String search) {
if (isNullOrEmpty(search)) {
return group -> true;
}
SearchRequest searchRequest = new SearchRequest(search, true);
return group -> SearchUtil.matchesOne(searchRequest, group.getName(), group.getDescription());
}
}

View File

@@ -45,8 +45,8 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
);
}
public Response getAll(int page, int pageSize, String sortBy, boolean desc, Function<PageResult<MODEL_OBJECT>, CollectionDto> mapToDto) {
return collectionAdapter.getAll(page, pageSize, sortBy, desc, mapToDto);
public Response getAll(int page, int pageSize, Predicate<MODEL_OBJECT> filter, String sortBy, boolean desc, Function<PageResult<MODEL_OBJECT>, CollectionDto> mapToDto) {
return collectionAdapter.getAll(page, pageSize, filter, sortBy, desc, mapToDto);
}
public Response create(DTO dto, Supplier<MODEL_OBJECT> modelObjectSupplier, Function<MODEL_OBJECT, String> uriCreator) {

View File

@@ -9,6 +9,8 @@ import org.apache.shiro.SecurityUtils;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.search.SearchRequest;
import sonia.scm.search.SearchUtil;
import sonia.scm.user.User;
import sonia.scm.web.VndMediaType;
@@ -23,6 +25,9 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import java.util.function.Predicate;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.util.Collections.singletonList;
public class RepositoryCollectionResource {
@@ -65,8 +70,10 @@ public class RepositoryCollectionResource {
public Response getAll(@DefaultValue("0") @QueryParam("page") int page,
@DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize,
@QueryParam("sortBy") String sortBy,
@DefaultValue("false") @QueryParam("desc") boolean desc) {
return adapter.getAll(page, pageSize, sortBy, desc,
@DefaultValue("false") @QueryParam("desc") boolean desc,
@DefaultValue("") @QueryParam("q") String search
) {
return adapter.getAll(page, pageSize, createSearchPredicate(search), sortBy, desc,
pageResult -> repositoryCollectionToDtoMapper.map(page, pageSize, pageResult));
}
@@ -106,4 +113,12 @@ public class RepositoryCollectionResource {
private String currentUser() {
return SecurityUtils.getSubject().getPrincipals().oneByType(User.class).getName();
}
private Predicate<Repository> createSearchPredicate(String search) {
if (isNullOrEmpty(search)) {
return user -> true;
}
SearchRequest searchRequest = new SearchRequest(search, true);
return repository -> SearchUtil.matchesOne(searchRequest, repository.getName(), repository.getNamespace(), repository.getDescription());
}
}

View File

@@ -5,11 +5,8 @@ import sonia.scm.ConcurrentModificationException;
import sonia.scm.Manager;
import sonia.scm.ModelObject;
import sonia.scm.NotFoundException;
import sonia.scm.api.rest.resources.AbstractManagerResource;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.Response;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -29,10 +26,11 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
*/
@SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right?
class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
DTO extends HalRepresentation> extends AbstractManagerResource<MODEL_OBJECT> {
DTO extends HalRepresentation> {
private final Function<Throwable, Optional<Response>> errorHandler;
private final Class<MODEL_OBJECT> type;
protected final Manager<MODEL_OBJECT> manager;
protected final Class<MODEL_OBJECT> type;
SingleResourceManagerAdapter(Manager<MODEL_OBJECT> manager, Class<MODEL_OBJECT> type) {
this(manager, type, e -> Optional.empty());
@@ -42,7 +40,7 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
Manager<MODEL_OBJECT> manager,
Class<MODEL_OBJECT> type,
Function<Throwable, Optional<Response>> errorHandler) {
super(manager, type);
this.manager = manager;
this.errorHandler = errorHandler;
this.type = type;
}
@@ -72,7 +70,16 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
else if (modelObjectWasModifiedConcurrently(existingModelObject, changedModelObject)) {
throw new ConcurrentModificationException(type, keyExtractor.apply(existingModelObject));
}
return update(getId(existingModelObject), changedModelObject);
return update(changedModelObject);
}
private Response update(MODEL_OBJECT item) {
try {
manager.modify(item);
return Response.noContent().build();
} catch (RuntimeException ex) {
return createErrorResponse(ex);
}
}
private boolean modelObjectWasModifiedConcurrently(MODEL_OBJECT existing, MODEL_OBJECT updated) {
@@ -89,23 +96,27 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
}
}
@Override
protected Response createErrorResponse(Throwable throwable) {
return errorHandler.apply(throwable).orElse(super.createErrorResponse(throwable));
public Response delete(String name) {
MODEL_OBJECT item = manager.get(name);
if (item != null) {
try {
manager.delete(item);
return Response.noContent().build();
} catch (RuntimeException ex) {
return createErrorResponse(ex);
}
} else {
return Response.noContent().build();
}
}
@Override
protected GenericEntity<Collection<MODEL_OBJECT>> createGenericEntity(Collection<MODEL_OBJECT> modelObjects) {
throw new UnsupportedOperationException();
private Response createErrorResponse(RuntimeException throwable) {
return errorHandler.apply(throwable)
.orElseThrow(() -> throwable);
}
@Override
protected String getId(MODEL_OBJECT item) {
return item.getId();
}
@Override
protected String getPathPart() {
throw new UnsupportedOperationException();
}
}

View File

@@ -6,6 +6,8 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.authc.credential.PasswordService;
import sonia.scm.search.SearchRequest;
import sonia.scm.search.SearchUtil;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
@@ -20,6 +22,9 @@ import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import java.util.function.Predicate;
import static com.google.common.base.Strings.isNullOrEmpty;
public class UserCollectionResource {
@@ -65,8 +70,10 @@ public class UserCollectionResource {
public Response getAll(@DefaultValue("0") @QueryParam("page") int page,
@DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize,
@QueryParam("sortBy") String sortBy,
@DefaultValue("false") @QueryParam("desc") boolean desc) {
return adapter.getAll(page, pageSize, sortBy, desc,
@DefaultValue("false") @QueryParam("desc") boolean desc,
@DefaultValue("") @QueryParam("q") String search
) {
return adapter.getAll(page, pageSize, createSearchPredicate(search), sortBy, desc,
pageResult -> userCollectionToDtoMapper.map(page, pageSize, pageResult));
}
@@ -93,4 +100,12 @@ public class UserCollectionResource {
public Response create(@Valid UserDto user) {
return adapter.create(user, () -> dtoToUserMapper.map(user, passwordService.encryptPassword(user.getPassword())), u -> resourceLinks.user().self(u.getName()));
}
private Predicate<User> createSearchPredicate(String search) {
if (isNullOrEmpty(search)) {
return user -> true;
}
SearchRequest searchRequest = new SearchRequest(search, true);
return user -> SearchUtil.matchesOne(searchRequest, user.getName(), user.getDisplayName(), user.getMail());
}
}

View File

@@ -59,6 +59,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Predicate;
//~--- JDK imports ------------------------------------------------------------
@@ -250,7 +251,7 @@ public class DefaultGroupManager extends AbstractGroupManager
@Override
public Collection<Group> getAll()
{
return getAll(null);
return getAll(group -> true, null);
}
/**
@@ -262,14 +263,14 @@ public class DefaultGroupManager extends AbstractGroupManager
* @return
*/
@Override
public Collection<Group> getAll(Comparator<Group> comparator)
public Collection<Group> getAll(Predicate<Group> filter, Comparator<Group> comparator)
{
List<Group> groups = new ArrayList<>();
PermissionActionCheck<Group> check = GroupPermissions.read();
for (Group group : groupDAO.getAll())
{
if (check.isPermitted(group)) {
if (filter.test(group) && check.isPermitted(group)) {
groups.add(group.clone());
}
}

View File

@@ -64,6 +64,7 @@ import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.function.Predicate;
import static sonia.scm.AlreadyExistsException.alreadyExists;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
@@ -253,13 +254,14 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
}
@Override
public Collection<Repository> getAll(Comparator<Repository> comparator) {
public Collection<Repository> getAll(Predicate<Repository> filter, Comparator<Repository> comparator) {
List<Repository> repositories = Lists.newArrayList();
PermissionActionCheck<Repository> check = RepositoryPermissions.read();
for (Repository repository : repositoryDAO.getAll()) {
if (handlerMap.containsKey(repository.getType())
&& filter.test(repository)
&& check.isPermitted(repository)) {
Repository r = repository.clone();
@@ -276,7 +278,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
@Override
public Collection<Repository> getAll() {
return getAll(null);
return getAll(repository -> true, null);
}

View File

@@ -1,221 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.search;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.cache.Cache;
import sonia.scm.security.ScmSecurityException;
import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.util.Collection;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response.Status;
import sonia.scm.security.Role;
/**
*
* @author Sebastian Sdorra
*
* @param <T>
*/
public class SearchHandler<T>
{
/** the logger for SearchHandler */
private static final Logger logger =
LoggerFactory.getLogger(SearchHandler.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param securityContextProvider
* @param cache
* @param searchable
*/
public SearchHandler(Cache<String, SearchResults> cache,
Searchable<T> searchable)
{
this.cache = cache;
this.searchable = searchable;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*/
public void clearCache()
{
this.cache.clear();
}
/**
* Method description
*
*
* @param queryString
* @param function
*
* @return
*/
public SearchResults search(String queryString,
Function<T, SearchResult> function)
{
Subject subject = SecurityUtils.getSubject();
if (!subject.hasRole(Role.USER))
{
throw new ScmSecurityException("Authentication is required");
}
if (Util.isEmpty(queryString))
{
throw new WebApplicationException(Status.BAD_REQUEST);
}
SearchResults result = cache.get(queryString);
if (result == null)
{
SearchRequest request = new SearchRequest(queryString, ignoreCase);
request.setMaxResults(maxResults);
Collection<T> users = searchable.search(request);
result = new SearchResults();
if (Util.isNotEmpty(users))
{
Collection<SearchResult> resultCollection =
Collections2.transform(users, function);
result.setSuccess(true);
// create a copy of the result collection to reduce memory
// use ArrayList instead of ImmutableList for copy,
// because the list must be mutable for decorators
result.setResults(Lists.newArrayList(resultCollection));
cache.put(queryString, result);
}
}
else if (logger.isDebugEnabled())
{
logger.debug("return searchresults for {} from cache", queryString);
}
return result;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public int getMaxResults()
{
return maxResults;
}
/**
* Method description
*
*
* @return
*/
public boolean isIgnoreCase()
{
return ignoreCase;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param ignoreCase
*/
public void setIgnoreCase(boolean ignoreCase)
{
this.ignoreCase = ignoreCase;
}
/**
* Method description
*
*
* @param maxResults
*/
public void setMaxResults(int maxResults)
{
this.maxResults = maxResults;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
protected Cache<String, SearchResults> cache;
/** Field description */
protected Searchable<T> searchable;
/** Field description */
private int maxResults = 5;
/** Field description */
private boolean ignoreCase = true;
}

View File

@@ -1,117 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.search;
/**
*
* @author Sebastian Sdorra
*/
public class SearchResult
{
/**
* Constructs ...
*
*/
public SearchResult() {}
/**
* Constructs ...
*
*
* @param value
* @param label
*/
public SearchResult(String value, String label)
{
this.value = value;
this.label = label;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public String getLabel()
{
return label;
}
/**
* Method description
*
*
* @return
*/
public String getValue()
{
return value;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param label
*/
public void setLabel(String label)
{
this.label = label;
}
/**
* Method description
*
*
* @param value
*/
public void setValue(String value)
{
this.value = value;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private String label;
/** Field description */
private String value;
}

View File

@@ -1,110 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.search;
//~--- JDK imports ------------------------------------------------------------
import java.util.Collection;
import javax.xml.bind.annotation.XmlRootElement;
import sonia.scm.api.rest.RestActionResult;
/**
*
* @author Sebastian Sdorra
*/
@XmlRootElement(name = "search-results")
public class SearchResults extends RestActionResult
{
/**
* Constructs ...
*
*/
public SearchResults() {}
/**
* Constructs ...
*
*
* @param success
*/
public SearchResults(boolean success)
{
super(success);
}
/**
* Constructs ...
*
*
* @param results
*/
public SearchResults(Collection<SearchResult> results)
{
super(true);
this.results = results;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public Collection<SearchResult> getResults()
{
return results;
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*
* @param results
*/
public void setResults(Collection<SearchResult> results)
{
this.results = results;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private Collection<SearchResult> results;
}

View File

@@ -62,6 +62,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
/**
*
@@ -280,7 +281,7 @@ public class DefaultUserManager extends AbstractUserManager
@Override
public Collection<User> getAll()
{
return getAll(null);
return getAll(user -> true, null);
}
/**
@@ -292,13 +293,13 @@ public class DefaultUserManager extends AbstractUserManager
* @return
*/
@Override
public Collection<User> getAll(Comparator<User> comparator)
public Collection<User> getAll(Predicate<User> filter, Comparator<User> comparator)
{
List<User> users = new ArrayList<>();
PermissionActionCheck<User> check = UserPermissions.read();
for (User user : userDAO.getAll()) {
if (check.isPermitted(user)) {
if (filter.test(user) && check.isPermitted(user)) {
users.add(user.clone());
}
}

View File

@@ -1,164 +0,0 @@
package sonia.scm.api.rest.resources;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.Manager;
import sonia.scm.ModelObject;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Comparator;
import static java.util.Collections.emptyList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class AbstractManagerResourceTest {
@Mock
private Manager<Simple> manager;
@Mock
private Request request;
@Mock
private UriInfo uriInfo;
@Captor
private ArgumentCaptor<Comparator<Simple>> comparatorCaptor;
private AbstractManagerResource<Simple> abstractManagerResource;
@Before
public void captureComparator() {
when(manager.getAll(comparatorCaptor.capture(), eq(0), eq(1))).thenReturn(emptyList());
abstractManagerResource = new SimpleManagerResource();
}
@Test
public void shouldAcceptDefaultSortByParameter() {
abstractManagerResource.getAll(request, 0, 1, null, true);
Comparator<Simple> comparator = comparatorCaptor.getValue();
assertTrue(comparator.compare(new Simple("1", null), new Simple("2", null)) > 0);
}
@Test
public void shouldAcceptValidSortByParameter() {
abstractManagerResource.getAll(request, 0, 1, "data", true);
Comparator<Simple> comparator = comparatorCaptor.getValue();
assertTrue(comparator.compare(new Simple("", "1"), new Simple("", "2")) > 0);
}
@Test(expected = IllegalArgumentException.class)
public void shouldFailForIllegalSortByParameter() {
abstractManagerResource.getAll(request, 0, 1, "x", true);
}
@Test
public void testLocation() throws URISyntaxException {
URI uri = location("special-item");
assertEquals(new URI("https://scm.scm-manager.org/simple/special-item"), uri);
}
@Test
public void testLocationWithSpaces() throws URISyntaxException {
URI uri = location("Scm Special Group");
assertEquals(new URI("https://scm.scm-manager.org/simple/Scm%20Special%20Group"), uri);
}
private URI location(String id) throws URISyntaxException {
URI base = new URI("https://scm.scm-manager.org/");
when(uriInfo.getAbsolutePath()).thenReturn(base);
return abstractManagerResource.location(uriInfo, id);
}
private class SimpleManagerResource extends AbstractManagerResource<Simple> {
{
disableCache = true;
}
private SimpleManagerResource() {
super(AbstractManagerResourceTest.this.manager, Simple.class);
}
@Override
protected GenericEntity<Collection<Simple>> createGenericEntity(Collection<Simple> items) {
return null;
}
@Override
protected String getId(Simple item) {
return null;
}
@Override
protected String getPathPart() {
return "simple";
}
}
public static class Simple implements ModelObject {
private String id;
private String data;
Simple(String id, String data) {
this.id = id;
this.data = data;
}
public String getData() {
return data;
}
@Override
public String getId() {
return id;
}
@Override
public void setLastModified(Long timestamp) {
}
@Override
public Long getCreationDate() {
return null;
}
@Override
public void setCreationDate(Long timestamp) {
}
@Override
public Long getLastModified() {
return null;
}
@Override
public String getType() {
return null;
}
@Override
public boolean isValid() {
return false;
}
}
}

View File

@@ -0,0 +1,52 @@
package sonia.scm.api.rest.resources;
import sonia.scm.ModelObject;
public class Simple implements ModelObject {
private String id;
private String data;
public Simple(String id, String data) {
this.id = id;
this.data = data;
}
public String getData() {
return data;
}
@Override
public String getId() {
return id;
}
@Override
public void setLastModified(Long timestamp) {
}
@Override
public Long getCreationDate() {
return null;
}
@Override
public void setCreationDate(Long timestamp) {
}
@Override
public Long getLastModified() {
return null;
}
@Override
public String getType() {
return null;
}
@Override
public boolean isValid() {
return false;
}
}

View File

@@ -0,0 +1,65 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.Manager;
import sonia.scm.api.rest.resources.Simple;
import java.util.Comparator;
import java.util.function.Predicate;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class CollectionResourceManagerAdapterTest {
@Mock
private Manager<Simple> manager;
@Captor
private ArgumentCaptor<Comparator<Simple>> comparatorCaptor;
@Captor
private ArgumentCaptor<Predicate<Simple>> filterCaptor;
private CollectionResourceManagerAdapter<Simple, HalRepresentation> abstractManagerResource;
@Before
public void captureComparator() {
when(manager.getPage(filterCaptor.capture(), comparatorCaptor.capture(), eq(0), eq(1))).thenReturn(null);
abstractManagerResource = new SimpleManagerResource();
}
@Test
public void shouldAcceptDefaultSortByParameter() {
abstractManagerResource.getAll(0, 1, x -> true, null, true, r -> null);
Comparator<Simple> comparator = comparatorCaptor.getValue();
assertTrue(comparator.compare(new Simple("1", null), new Simple("2", null)) > 0);
}
@Test
public void shouldAcceptValidSortByParameter() {
abstractManagerResource.getAll(0, 1, x -> true, "data", true, r -> null);
Comparator<Simple> comparator = comparatorCaptor.getValue();
assertTrue(comparator.compare(new Simple("", "1"), new Simple("", "2")) > 0);
}
@Test(expected = IllegalArgumentException.class)
public void shouldFailForIllegalSortByParameter() {
abstractManagerResource.getAll(0, 1, x -> true, "x", true, r -> null);
}
private class SimpleManagerResource extends CollectionResourceManagerAdapter<Simple, HalRepresentation> {
private SimpleManagerResource() {
super(CollectionResourceManagerAdapterTest.this.manager, Simple.class);
}
}
}

View File

@@ -11,6 +11,7 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.PageResult;
@@ -30,9 +31,11 @@ import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.function.Predicate;
import static java.util.Collections.singletonList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -67,8 +70,10 @@ public class GroupRootResourceTest {
@InjectMocks
private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper;
private ArgumentCaptor<Group> groupCaptor = ArgumentCaptor.forClass(Group.class);
@Captor
private ArgumentCaptor<Group> groupCaptor;
@Captor
private ArgumentCaptor<Predicate<Group>> filterCaptor;
@Before
public void prepareEnvironment() {
@@ -77,7 +82,7 @@ public class GroupRootResourceTest {
doNothing().when(groupManager).modify(groupCaptor.capture());
Group group = createDummyGroup();
when(groupManager.getPage(any(), eq(0), eq(10))).thenReturn(new PageResult<>(singletonList(group), 1));
when(groupManager.getPage(filterCaptor.capture(), any(), eq(0), eq(10))).thenReturn(new PageResult<>(singletonList(group), 1));
when(groupManager.get("admin")).thenReturn(group);
GroupCollectionToDtoMapper groupCollectionToDtoMapper = new GroupCollectionToDtoMapper(groupToDtoMapper, resourceLinks);
@@ -317,6 +322,23 @@ public class GroupRootResourceTest {
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/groups/admin\"}"));
}
@Test
public void shouldCreateFilterForSearch() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "?q=One");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
Group group = new Group("xml", "someone");
assertTrue(filterCaptor.getValue().test(group));
group.setName("nothing");
group.setDescription("Someone");
assertTrue(filterCaptor.getValue().test(group));
group.setDescription("Nobody");
assertFalse(filterCaptor.getValue().test(group));
}
@Test
public void shouldGetPermissionLink() throws URISyntaxException, UnsupportedEncodingException {
MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "admin");

View File

@@ -13,6 +13,7 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.PageResult;
@@ -31,6 +32,7 @@ import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.function.Predicate;
import static java.util.Collections.singletonList;
import static java.util.stream.Stream.of;
@@ -42,6 +44,7 @@ import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyObject;
@@ -78,6 +81,8 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
@Mock
private ScmPathInfo uriInfo;
@Captor
private ArgumentCaptor<Predicate<Repository>> filterCaptor;
private final URI baseUri = URI.create("/");
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@@ -150,7 +155,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
@Test
public void shouldGetAll() throws URISyntaxException, UnsupportedEncodingException {
PageResult<Repository> singletonPageResult = createSingletonPageResult(mockRepository("space", "repo"));
when(repositoryManager.getPage(any(), eq(0), eq(10))).thenReturn(singletonPageResult);
when(repositoryManager.getPage(any(), any(), eq(0), eq(10))).thenReturn(singletonPageResult);
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2);
MockHttpResponse response = new MockHttpResponse();
@@ -161,6 +166,22 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
assertTrue(response.getContentAsString().contains("\"name\":\"repo\""));
}
@Test
public void shouldCreateFilterForSearch() throws URISyntaxException {
PageResult<Repository> singletonPageResult = createSingletonPageResult(mockRepository("space", "repo"));
when(repositoryManager.getPage(filterCaptor.capture(), any(), eq(0), eq(10))).thenReturn(singletonPageResult);
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "?q=Rep");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(SC_OK, response.getStatus());
assertTrue(filterCaptor.getValue().test(new Repository("x", "git", "all_repos", "x")));
assertTrue(filterCaptor.getValue().test(new Repository("x", "git", "x", "repository")));
assertFalse(filterCaptor.getValue().test(new Repository("rep", "rep", "x", "x")));
}
@Test
public void shouldHandleUpdateForNotExistingRepository() throws URISyntaxException, IOException {
URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json");

View File

@@ -12,6 +12,7 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.ContextEntry;
@@ -30,6 +31,7 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collection;
import java.util.function.Predicate;
import static java.util.Collections.singletonList;
import static org.junit.Assert.assertEquals;
@@ -72,7 +74,11 @@ public class UserRootResourceTest {
@InjectMocks
private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper;
private ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
@Captor
private ArgumentCaptor<User> userCaptor;
@Captor
private ArgumentCaptor<Predicate<User>> filterCaptor;
private User originalUser;
@Before
@@ -333,7 +339,7 @@ public class UserRootResourceTest {
@Test
public void shouldCreatePageForOnePageOnly() throws URISyntaxException, UnsupportedEncodingException {
PageResult<User> singletonPageResult = createSingletonPageResult(1);
when(userManager.getPage(any(), eq(0), eq(10))).thenReturn(singletonPageResult);
when(userManager.getPage(any(), any(), eq(0), eq(10))).thenReturn(singletonPageResult);
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2);
MockHttpResponse response = new MockHttpResponse();
@@ -349,7 +355,7 @@ public class UserRootResourceTest {
@Test
public void shouldCreatePageForMultiplePages() throws URISyntaxException, UnsupportedEncodingException {
PageResult<User> singletonPageResult = createSingletonPageResult(3);
when(userManager.getPage(any(), eq(1), eq(1))).thenReturn(singletonPageResult);
when(userManager.getPage(any(), any(), eq(1), eq(1))).thenReturn(singletonPageResult);
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "?page=1&pageSize=1");
MockHttpResponse response = new MockHttpResponse();
@@ -364,6 +370,28 @@ public class UserRootResourceTest {
assertTrue(response.getContentAsString().contains("\"last\":{\"href\":\"/v2/users/?page=2"));
}
@Test
public void shouldCreateFilterForSearch() throws URISyntaxException {
PageResult<User> singletonPageResult = createSingletonPageResult(1);
when(userManager.getPage(filterCaptor.capture(), any(), eq(0), eq(10))).thenReturn(singletonPageResult);
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "?q=One");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
User user = new User("Someone I know");
assertTrue(filterCaptor.getValue().test(user));
user.setName("nobody");
user.setDisplayName("Someone I know");
assertTrue(filterCaptor.getValue().test(user));
user.setDisplayName("nobody");
user.setMail("me@someone.com");
assertTrue(filterCaptor.getValue().test(user));
user.setMail("me@nowhere.com");
assertFalse(filterCaptor.getValue().test(user));
}
@Test
public void shouldGetPermissionLink() throws URISyntaxException, UnsupportedEncodingException {
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo");