mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 00:15:44 +01:00
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"check": "flow check"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.26"
|
||||
"@scm-manager/ui-bundler": "^0.0.28"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"groups": "Gruppen",
|
||||
"config": "Einstellungen"
|
||||
},
|
||||
"filterEntries": "Einträge filtern",
|
||||
"paginator": {
|
||||
"next": "Weiter",
|
||||
"previous": "Zurück"
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"setPermissionsNavLink": "Berechtigungen"
|
||||
}
|
||||
},
|
||||
"addUser": {
|
||||
"createUser": {
|
||||
"title": "Benutzer erstellen",
|
||||
"subtitle": "Erstellen eines neuen Benutzers"
|
||||
},
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"groups": "Groups",
|
||||
"config": "Configuration"
|
||||
},
|
||||
"filterEntries": "filter entries",
|
||||
"paginator": {
|
||||
"next": "Next",
|
||||
"previous": "Previous"
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"setPermissionsNavLink": "Permissions"
|
||||
}
|
||||
},
|
||||
"addUser": {
|
||||
"createUser": {
|
||||
"title": "Create User",
|
||||
"subtitle": "Create a new user"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -40,7 +40,8 @@ class GroupForm extends React.Component<Props, State> {
|
||||
},
|
||||
_links: {},
|
||||
members: [],
|
||||
type: ""
|
||||
type: "",
|
||||
external: false
|
||||
},
|
||||
nameValidationError: false
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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)));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user