Merged in feature/search (pull request #231)

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

View File

@@ -35,6 +35,7 @@ package sonia.scm;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.function.Predicate;
/** /**
* Base interface for all manager classes. * 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} * 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 * @param comparator to sort the returned objects
* @since 1.4 * @since 1.4
* @return all object of the store sorted by the given {@link java.util.Comparator} * @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 * 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 * <p>This default implementation reads all items, first, so you might want to adapt this
* whenever reading is expensive!</p> * whenever reading is expensive!</p>
* *
* @param filter to filter returned objects
* @param comparator to sort the returned objects * @param comparator to sort the returned objects
* @param pageNumber the number of the page to be returned (zero based) * @param pageNumber the number of the page to be returned (zero based)
* @param pageSize the size of the pages * @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 * page. If the requested page number exceeds the existing pages, an
* empty page result is returned. * empty page result is returned.
*/ */
default PageResult<T> getPage(Comparator<T> comparator, int pageNumber, int pageSize) { default PageResult<T> getPage(Predicate<T> filter, Comparator<T> comparator, int pageNumber, int pageSize) {
return PageResult.createPage(getAll(comparator), pageNumber, pageSize); return PageResult.createPage(getAll(filter, comparator), pageNumber, pageSize);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ export { default as AutocompleteAddEntryToTableField } from "./AutocompleteAddEn
export { default as MemberNameTable } from "./MemberNameTable.js"; export { default as MemberNameTable } from "./MemberNameTable.js";
export { default as Checkbox } from "./Checkbox.js"; export { default as Checkbox } from "./Checkbox.js";
export { default as Radio } from "./Radio.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 InputField } from "./InputField.js";
export { default as Select } from "./Select.js"; export { default as Select } from "./Select.js";
export { default as Textarea } from "./Textarea.js"; export { default as Textarea } from "./Textarea.js";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -693,9 +693,9 @@
version "0.0.2" version "0.0.2"
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
"@scm-manager/ui-bundler@^0.0.26": "@scm-manager/ui-bundler@^0.0.28":
version "0.0.26" version "0.0.28"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66" resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e"
dependencies: dependencies:
"@babel/core" "^7.0.0" "@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0"
@@ -6530,6 +6530,14 @@ qs@~6.5.2:
version "6.5.2" version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" 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: querystring-es3@~0.2.0:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" 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" commander "^2.2.0"
limiter "^1.0.5" 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: string-length@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,9 +40,14 @@ export function fetchGroups(link: string) {
return fetchGroupsByLink(link); return fetchGroupsByLink(link);
} }
export function fetchGroupsByPage(link: string, page: number) { export function fetchGroupsByPage(link: string, page: number, filter?: string) {
// backend start counting by 0 // 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) { export function fetchGroupsByLink(link: string) {

View File

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

View File

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

View File

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

View File

@@ -46,7 +46,12 @@ export function fetchRepos(link: string) {
return fetchReposByLink(link); 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}`); return fetchReposByLink(`${link}?page=${page - 1}`);
} }

View File

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

View File

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

View File

@@ -43,9 +43,14 @@ export function fetchUsers(link: string) {
return fetchUsersByLink(link); return fetchUsersByLink(link);
} }
export function fetchUsersByPage(link: string, page: number) { export function fetchUsersByPage(link: string, page: number, filter?: string) {
// backend start counting by 0 // 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) { export function fetchUsersByLink(link: string) {
@@ -153,9 +158,7 @@ export function createUser(link: string, user: User, callback?: () => void) {
callback(); callback();
} }
}) })
.catch(error => .catch(error => dispatch(createUserFailure(error)));
dispatch(createUserFailure(error))
);
}; };
} }

View File

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

View File

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

View File

@@ -52,12 +52,7 @@
]]> ]]>
</description> </description>
<api-classes> <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>
<modules> <modules>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,13 +4,15 @@ import de.otto.edison.hal.HalRepresentation;
import sonia.scm.Manager; import sonia.scm.Manager;
import sonia.scm.ModelObject; import sonia.scm.ModelObject;
import sonia.scm.PageResult; 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 javax.ws.rs.core.Response;
import java.net.URI; import java.net.URI;
import java.util.Collection; import java.util.Comparator;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
import static javax.ws.rs.core.Response.Status.BAD_REQUEST; 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? @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right?
class CollectionResourceManagerAdapter<MODEL_OBJECT extends ModelObject, 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) { 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. * 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. * 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) { 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(sortBy, desc, page, pageSize); PageResult<MODEL_OBJECT> pageResult = fetchPage(filter, sortBy, desc, page, pageSize);
return Response.ok(mapToDto.apply(pageResult)).build(); 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. * 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. * 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(); 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) { protected String getId(MODEL_OBJECT item) {
return item.getId(); return item.getId();
} }
@Override
protected String getPathPart() {
throw new UnsupportedOperationException();
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@ import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import sonia.scm.PageResult; import sonia.scm.PageResult;
@@ -30,9 +31,11 @@ import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.function.Predicate;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@@ -67,8 +70,10 @@ public class GroupRootResourceTest {
@InjectMocks @InjectMocks
private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper;
@Captor
private ArgumentCaptor<Group> groupCaptor = ArgumentCaptor.forClass(Group.class); private ArgumentCaptor<Group> groupCaptor;
@Captor
private ArgumentCaptor<Predicate<Group>> filterCaptor;
@Before @Before
public void prepareEnvironment() { public void prepareEnvironment() {
@@ -77,7 +82,7 @@ public class GroupRootResourceTest {
doNothing().when(groupManager).modify(groupCaptor.capture()); doNothing().when(groupManager).modify(groupCaptor.capture());
Group group = createDummyGroup(); 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); when(groupManager.get("admin")).thenReturn(group);
GroupCollectionToDtoMapper groupCollectionToDtoMapper = new GroupCollectionToDtoMapper(groupToDtoMapper, resourceLinks); GroupCollectionToDtoMapper groupCollectionToDtoMapper = new GroupCollectionToDtoMapper(groupToDtoMapper, resourceLinks);
@@ -317,6 +322,23 @@ public class GroupRootResourceTest {
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/groups/admin\"}")); 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 @Test
public void shouldGetPermissionLink() throws URISyntaxException, UnsupportedEncodingException { public void shouldGetPermissionLink() throws URISyntaxException, UnsupportedEncodingException {
MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "admin"); MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "admin");

View File

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

View File

@@ -12,6 +12,7 @@ import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import sonia.scm.ContextEntry; import sonia.scm.ContextEntry;
@@ -30,6 +31,7 @@ import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.util.Collection; import java.util.Collection;
import java.util.function.Predicate;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -72,7 +74,11 @@ public class UserRootResourceTest {
@InjectMocks @InjectMocks
private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; 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; private User originalUser;
@Before @Before
@@ -333,7 +339,7 @@ public class UserRootResourceTest {
@Test @Test
public void shouldCreatePageForOnePageOnly() throws URISyntaxException, UnsupportedEncodingException { public void shouldCreatePageForOnePageOnly() throws URISyntaxException, UnsupportedEncodingException {
PageResult<User> singletonPageResult = createSingletonPageResult(1); 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); MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2);
MockHttpResponse response = new MockHttpResponse(); MockHttpResponse response = new MockHttpResponse();
@@ -349,7 +355,7 @@ public class UserRootResourceTest {
@Test @Test
public void shouldCreatePageForMultiplePages() throws URISyntaxException, UnsupportedEncodingException { public void shouldCreatePageForMultiplePages() throws URISyntaxException, UnsupportedEncodingException {
PageResult<User> singletonPageResult = createSingletonPageResult(3); 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"); MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "?page=1&pageSize=1");
MockHttpResponse response = new MockHttpResponse(); MockHttpResponse response = new MockHttpResponse();
@@ -364,6 +370,28 @@ public class UserRootResourceTest {
assertTrue(response.getContentAsString().contains("\"last\":{\"href\":\"/v2/users/?page=2")); 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 @Test
public void shouldGetPermissionLink() throws URISyntaxException, UnsupportedEncodingException { public void shouldGetPermissionLink() throws URISyntaxException, UnsupportedEncodingException {
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo"); MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo");