Compare commits

...

20 Commits
3.4.0 ... 3.5.0

Author SHA1 Message Date
Athou
84626e1ef2 release 3.5.0 2023-06-01 07:49:42 +02:00
Athou
191ece0bac update browser extensions link 2023-06-01 07:43:17 +02:00
Athou
aa5e9bfd83 update readme to point to the new browser extension 2023-05-31 17:55:47 +02:00
Athou
a200147926 remove X-Frame-Options: DENY as it blocks the iframe from the future browser extension 2023-05-31 15:24:17 +02:00
Athou
d6205b7da3 fix typo 2023-05-31 07:36:50 +02:00
Athou
5ecf3e0fbf add setting to disable strict password policy (#1059) 2023-05-31 07:31:40 +02:00
Athou
bb25e0ede6 intellij autofixes 2023-05-31 07:27:24 +02:00
Athou
f5c0e2d375 update documentation: alphabetical ordering is no longer available 2023-05-30 21:22:02 +02:00
Athou
12ab5b1e7b add default value to allow app startup even if the setting is missing in config.yml 2023-05-30 10:53:28 +02:00
Athou
3e6451289f add setting to limit feeds per user 2023-05-30 09:10:20 +02:00
Athou
09d21d88a4 remove usage of deprecated id generator that blocks migration to hibernate 6 2023-05-29 20:36:45 +02:00
Athou
2ec6d0a66a api documentation page no longer requires users to be authenticated 2023-05-28 22:38:57 +02:00
Athou
412fc52f1c add css classes to help with custom css rules (#1061) 2023-05-28 12:57:28 +02:00
Athou
b5e5989604 disable pull-to-refresh on mobile as it messes with vertical scrolling 2023-05-27 19:54:02 +02:00
Athou
105ff46c01 UnitOfWork is now injectable 2023-05-27 19:46:49 +02:00
Athou
f100f3f91a run post login activities in a new transaction to avoid database locks 2023-05-27 19:29:44 +02:00
Athou
45eb436b8f reduce chance of deadlocks 2023-05-27 08:38:23 +02:00
Athou
bf3914e748 remove very slow query 2023-05-27 08:03:36 +02:00
Athou
5df7aaf7cd try to fix redis timeouts (#1060) 2023-05-27 07:58:22 +02:00
Athou
f10cfd7ad0 add feed refresh engine metrics 2023-05-26 15:20:17 +02:00
64 changed files with 344 additions and 248 deletions

View File

@@ -1,5 +1,16 @@
# Changelog # Changelog
## [3.5.0]
- add compatibility with the new version of the CommaFeed browser extension
- disable pull-to-refresh on mobile as it messes with vertical scrolling
- add css classes to feed entries to help with custom css rules
- api documentation page no longer requires users to be authenticated
- add a setting to limit the number of feeds a user can subscribe to
- add a setting to disable strict password policy
- add feed refresh engine metrics
- fix redis timeouts
## [3.4.0] ## [3.4.0]
- add support for arm64 docker images - add support for arm64 docker images

View File

@@ -15,15 +15,7 @@ Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/Typ
- Supports thousands of users and millions of feeds - Supports thousands of users and millions of feeds
- OPML import/export - OPML import/export
- REST API - REST API
- [Browser extension](https://github.com/Athou/commafeed-browser-extension)
## Related open-source projects
Browser extensions:
- [Chrome](https://github.com/Athou/commafeed-chrome)
- [Firefox](https://github.com/Athou/commafeed-firefox)
- [Opera](https://github.com/Athou/commafeed-opera)
- [Safari](https://github.com/Athou/commafeed-safari)
## Deployment on your own server ## Deployment on your own server

View File

@@ -5,7 +5,7 @@
<parent> <parent>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>3.4.0</version> <version>3.5.0</version>
</parent> </parent>
<artifactId>commafeed-client</artifactId> <artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name> <name>CommaFeed Client</name>

View File

@@ -72,6 +72,7 @@ function AppRoutes() {
<Route path="login" element={<LoginPage />} /> <Route path="login" element={<LoginPage />} />
<Route path="register" element={<RegistrationPage />} /> <Route path="register" element={<RegistrationPage />} />
<Route path="passwordRecovery" element={<PasswordRecoveryPage />} /> <Route path="passwordRecovery" element={<PasswordRecoveryPage />} />
<Route path="api" element={<ApiDocumentationPage />} />
<Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} />}> <Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} />}>
<Route path="category"> <Route path="category">
<Route path=":id" element={<FeedEntriesPage sourceType="category" />} /> <Route path=":id" element={<FeedEntriesPage sourceType="category" />} />
@@ -93,7 +94,6 @@ function AppRoutes() {
</Route> </Route>
<Route path="about" element={<AboutPage />} /> <Route path="about" element={<AboutPage />} />
<Route path="donate" element={<DonatePage />} /> <Route path="donate" element={<DonatePage />} />
<Route path="api" element={<ApiDocumentationPage />} />
</Route> </Route>
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>

View File

@@ -13,6 +13,8 @@ export const redirectToRegistration = createAsyncThunk("redirect/register", (_,
export const redirectToPasswordRecovery = createAsyncThunk("redirect/passwordRecovery", (_, thunkApi) => export const redirectToPasswordRecovery = createAsyncThunk("redirect/passwordRecovery", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/passwordRecovery")) thunkApi.dispatch(redirectTo("/passwordRecovery"))
) )
export const redirectToApiDocumentation = createAsyncThunk("redirect/api", (_, thunkApi) => thunkApi.dispatch(redirectTo("/api")))
export const redirectToSelectedSource = createAsyncThunk< export const redirectToSelectedSource = createAsyncThunk<
void, void,
void, void,
@@ -52,7 +54,6 @@ export const redirectToMetrics = createAsyncThunk("redirect/admin/metrics", (_,
) )
export const redirectToDonate = createAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate"))) export const redirectToDonate = createAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate")))
export const redirectToAbout = createAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about"))) export const redirectToAbout = createAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about")))
export const redirectToApiDocumentation = createAsyncThunk("redirect/api", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/api")))
export const redirectSlice = createSlice({ export const redirectSlice = createSlice({
name: "redirect", name: "redirect",

View File

@@ -263,6 +263,7 @@ export function FeedEntries() {
<FeedEntry <FeedEntry
entry={entry} entry={entry}
expanded={!!entry.expanded || viewMode === "expanded"} expanded={!!entry.expanded || viewMode === "expanded"}
selected={entry.id === selectedEntryId}
showSelectionIndicator={entry.id === selectedEntryId && (!entry.expanded || viewMode === "expanded")} showSelectionIndicator={entry.id === selectedEntryId && (!entry.expanded || viewMode === "expanded")}
onHeaderClick={event => headerClicked(entry, event)} onHeaderClick={event => headerClicked(entry, event)}
/> />

View File

@@ -16,6 +16,7 @@ import { FeedEntryHeader } from "./FeedEntryHeader"
interface FeedEntryProps { interface FeedEntryProps {
entry: Entry entry: Entry
expanded: boolean expanded: boolean
selected: boolean
showSelectionIndicator: boolean showSelectionIndicator: boolean
onHeaderClick: (e: React.MouseEvent) => void onHeaderClick: (e: React.MouseEvent) => void
} }
@@ -72,7 +73,7 @@ const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: View
export function FeedEntry(props: FeedEntryProps) { export function FeedEntry(props: FeedEntryProps) {
const { viewMode } = useViewMode() const { viewMode } = useViewMode()
const { classes } = useStyles({ ...props, viewMode }) const { classes, cx } = useStyles({ ...props, viewMode })
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@@ -95,7 +96,17 @@ export function FeedEntry(props: FeedEntryProps) {
const compactHeader = !props.expanded && (viewMode === "title" || viewMode === "cozy") const compactHeader = !props.expanded && (viewMode === "title" || viewMode === "cozy")
return ( return (
<Paper withBorder radius={borderRadius} className={classes.paper}> <Paper
withBorder
radius={borderRadius}
className={cx(classes.paper, {
read: props.entry.read,
unread: !props.entry.read,
expanded: props.expanded,
selected: props.selected,
"show-selection-indicator": props.showSelectionIndicator,
})}
>
<a <a
className={classes.headerLink} className={classes.headerLink}
href={props.entry.url} href={props.entry.url}

View File

@@ -0,0 +1,4 @@
html, body {
/* disable pull-to-refresh on mobile as it messes with vertical scrolling */
overscroll-behavior: none;
}

View File

@@ -1,11 +1,12 @@
import "@fontsource/open-sans" import "@fontsource/open-sans"
import { App } from "App"
import { store } from "app/store" import { store } from "app/store"
import dayjs from "dayjs" import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime" import relativeTime from "dayjs/plugin/relativeTime"
import "main.css"
import "react-contexify/ReactContexify.css" import "react-contexify/ReactContexify.css"
import ReactDOM from "react-dom/client" import ReactDOM from "react-dom/client"
import { Provider } from "react-redux" import { Provider } from "react-redux"
import { App } from "./App"
dayjs.extend(relativeTime) dayjs.extend(relativeTime)

View File

@@ -1,6 +1,7 @@
import { Accordion, Tabs } from "@mantine/core" import { Accordion, Box, Tabs } from "@mantine/core"
import { client } from "app/client" import { client } from "app/client"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { Gauge } from "components/metrics/Gauge"
import { Meter } from "components/metrics/Meter" import { Meter } from "components/metrics/Meter"
import { MetricAccordionItem } from "components/metrics/MetricAccordionItem" import { MetricAccordionItem } from "components/metrics/MetricAccordionItem"
import { Timer } from "components/metrics/Timer" import { Timer } from "components/metrics/Timer"
@@ -15,11 +16,17 @@ const shownMeters: { [key: string]: string } = {
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss": "Entry cache miss rate", "com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss": "Entry cache miss rate",
} }
const shownGauges: { [key: string]: string } = {
"com.commafeed.backend.feed.FeedRefreshEngine.queue.size": "Queue size",
"com.commafeed.backend.feed.FeedRefreshEngine.worker.active": "Feed Worker active",
"com.commafeed.backend.feed.FeedRefreshEngine.updater.active": "Feed Updater active",
}
export function MetricsPage() { export function MetricsPage() {
const query = useAsync(() => client.admin.getMetrics(), []) const query = useAsync(() => client.admin.getMetrics(), [])
if (!query.result) return <Loader /> if (!query.result) return <Loader />
const { meters, timers } = query.result.data const { meters, gauges, timers } = query.result.data
return ( return (
<Tabs defaultValue="stats"> <Tabs defaultValue="stats">
<Tabs.List> <Tabs.List>
@@ -39,6 +46,15 @@ export function MetricsPage() {
</MetricAccordionItem> </MetricAccordionItem>
))} ))}
</Accordion> </Accordion>
<Box pt="xs">
{Object.keys(shownGauges).map(g => (
<Box key={g}>
<span>{shownGauges[g]}&nbsp;</span>
<Gauge gauge={gauges[g]} />
</Box>
))}
</Box>
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="timers" pt="xs"> <Tabs.Panel value="timers" pt="xs">

View File

@@ -86,28 +86,9 @@ export function AboutPage() {
<Section title={<Trans>Goodies</Trans>} icon={<TbPuzzle size={24} />}> <Section title={<Trans>Goodies</Trans>} icon={<TbPuzzle size={24} />}>
<List> <List>
<List.Item> <List.Item>
<Trans>Browser extentions</Trans> <Anchor href="https://github.com/Athou/commafeed-browser-extension" target="_blank" rel="noreferrer">
<List withPadding> <Trans>Browser extentions</Trans>
<List.Item> </Anchor>
<Anchor
href="https://addons.mozilla.org/en-US/firefox/addon/commafeed/"
target="_blank"
rel="noreferrer"
>
Firefox
</Anchor>
</List.Item>
<List.Item>
<Anchor href="https://github.com/Athou/commafeed-chrome" target="_blank" rel="noreferrer">
Chrome
</Anchor>
</List.Item>
<List.Item>
<Anchor href="https://github.com/Athou/commafeed-opera" target="_blank" rel="noreferrer">
Opera
</Anchor>
</List.Item>
</List>
</List.Item> </List.Item>
<List.Item> <List.Item>
<Trans>Subscribe URL</Trans> <Trans>Subscribe URL</Trans>

View File

@@ -4,8 +4,11 @@ app:
# url used to access commafeed # url used to access commafeed
publicUrl: http://localhost:8082/ publicUrl: http://localhost:8082/
# wether to allow user registrations # whether to allow user registrations
allowRegistrations: true allowRegistrations: true
# whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char)
strictPasswordPolicy: true
# create a demo account the first time the app starts # create a demo account the first time the app starts
createDemoAccount: true createDemoAccount: true
@@ -37,14 +40,14 @@ app:
graphitePort: 2003 graphitePort: 2003
graphiteInterval: 60 graphiteInterval: 60
# wether this commafeed instance has a lot of feeds to refresh # whether this commafeed instance has a lot of feeds to refresh
# leave this to false in almost all cases # leave this to false in almost all cases
heavyLoad: false heavyLoad: false
# minimum amount of time commafeed will wait before refreshing the same feed # minimum amount of time commafeed will wait before refreshing the same feed
refreshIntervalMinutes: 5 refreshIntervalMinutes: 5
# wether to enable pubsub # whether to enable pubsub
# probably not needed if refreshIntervalMinutes is low # probably not needed if refreshIntervalMinutes is low
pubsubhubbub: false pubsubhubbub: false
@@ -60,7 +63,10 @@ app:
# entries to keep per feed, old entries will be deleted, 0 to disable # entries to keep per feed, old entries will be deleted, 0 to disable
maxFeedCapacity: 500 maxFeedCapacity: 500
# limit the number of feeds a user can subscribe to, 0 to disable
maxFeedsPerUser: 0
# cache service to use, possible values are 'noop' and 'redis' # cache service to use, possible values are 'noop' and 'redis'
cache: noop cache: noop

View File

@@ -6,6 +6,9 @@ app:
# whether to allow user registrations # whether to allow user registrations
allowRegistrations: false allowRegistrations: false
# whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char)
strictPasswordPolicy: true
# create a demo account the first time the app starts # create a demo account the first time the app starts
createDemoAccount: false createDemoAccount: false
@@ -38,14 +41,14 @@ app:
graphitePort: 2003 graphitePort: 2003
graphiteInterval: 60 graphiteInterval: 60
# wether this commafeed instance has a lot of feeds to refresh # whether this commafeed instance has a lot of feeds to refresh
# leave this to false in almost all cases # leave this to false in almost all cases
heavyLoad: false heavyLoad: false
# minimum amount of time commafeed will wait before refreshing the same feed # minimum amount of time commafeed will wait before refreshing the same feed
refreshIntervalMinutes: 5 refreshIntervalMinutes: 5
# wether to enable pubsub # whether to enable pubsub
# probably not needed if refreshIntervalMinutes is low # probably not needed if refreshIntervalMinutes is low
pubsubhubbub: false pubsubhubbub: false
@@ -61,7 +64,10 @@ app:
# entries to keep per feed, old entries will be deleted, 0 to disable # entries to keep per feed, old entries will be deleted, 0 to disable
maxFeedCapacity: 500 maxFeedCapacity: 500
# limit the number of feeds a user can subscribe to, 0 to disable
maxFeedsPerUser: 0
# cache service to use, possible values are 'noop' and 'redis' # cache service to use, possible values are 'noop' and 'redis'
cache: noop cache: noop

View File

@@ -6,7 +6,7 @@
<parent> <parent>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>3.4.0</version> <version>3.5.0</version>
</parent> </parent>
<artifactId>commafeed-server</artifactId> <artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name> <name>CommaFeed Server</name>
@@ -226,7 +226,7 @@
<dependency> <dependency>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId> <artifactId>commafeed-client</artifactId>
<version>3.4.0</version> <version>3.5.0</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -278,11 +278,6 @@
<groupId>io.dropwizard.metrics</groupId> <groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-json</artifactId> <artifactId>metrics-json</artifactId>
</dependency> </dependency>
<dependency>
<groupId>io.dropwizard.modules</groupId>
<artifactId>dropwizard-web</artifactId>
<version>1.5.0</version>
</dependency>
<dependency> <dependency>
<groupId>be.tomcools</groupId> <groupId>be.tomcools</groupId>
<artifactId>dropwizard-websocket-jee7-bundle</artifactId> <artifactId>dropwizard-websocket-jee7-bundle</artifactId>
@@ -374,7 +369,7 @@
<dependency> <dependency>
<groupId>redis.clients</groupId> <groupId>redis.clients</groupId>
<artifactId>jedis</artifactId> <artifactId>jedis</artifactId>
<version>4.3.2</version> <version>4.4.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.sun.mail</groupId> <groupId>com.sun.mail</groupId>

View File

@@ -33,6 +33,7 @@ import com.commafeed.backend.model.UserSettings;
import com.commafeed.backend.service.DatabaseStartupService; import com.commafeed.backend.service.DatabaseStartupService;
import com.commafeed.backend.service.UserService; import com.commafeed.backend.service.UserService;
import com.commafeed.backend.task.ScheduledTask; import com.commafeed.backend.task.ScheduledTask;
import com.commafeed.frontend.auth.PasswordConstraintValidator;
import com.commafeed.frontend.auth.SecurityCheckFactoryProvider; import com.commafeed.frontend.auth.SecurityCheckFactoryProvider;
import com.commafeed.frontend.resource.AdminREST; import com.commafeed.frontend.resource.AdminREST;
import com.commafeed.frontend.resource.CategoryREST; import com.commafeed.frontend.resource.CategoryREST;
@@ -69,8 +70,6 @@ import io.dropwizard.migrations.MigrationsBundle;
import io.dropwizard.servlets.CacheBustingFilter; import io.dropwizard.servlets.CacheBustingFilter;
import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment; import io.dropwizard.setup.Environment;
import io.dropwizard.web.WebBundle;
import io.dropwizard.web.conf.WebConfiguration;
import io.whitfin.dropwizard.configuration.EnvironmentSubstitutor; import io.whitfin.dropwizard.configuration.EnvironmentSubstitutor;
public class CommaFeedApplication extends Application<CommaFeedConfiguration> { public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
@@ -118,24 +117,16 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) { public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
DataSourceFactory factory = configuration.getDataSourceFactory(); DataSourceFactory factory = configuration.getDataSourceFactory();
// keep using old id generator for backward compatibility factory.getProperties().put(AvailableSettings.PREFERRED_POOLED_OPTIMIZER, "pooled-lo");
factory.getProperties().put(AvailableSettings.USE_NEW_ID_GENERATOR_MAPPINGS, "false");
factory.getProperties().put(AvailableSettings.STATEMENT_BATCH_SIZE, "50"); factory.getProperties().put(AvailableSettings.STATEMENT_BATCH_SIZE, "50");
factory.getProperties().put(AvailableSettings.BATCH_VERSIONED_DATA, "true"); factory.getProperties().put(AvailableSettings.BATCH_VERSIONED_DATA, "true");
factory.getProperties().put(AvailableSettings.ORDER_INSERTS, "true");
factory.getProperties().put(AvailableSettings.ORDER_UPDATES, "true");
return factory; return factory;
} }
}); });
bootstrap.addBundle(new WebBundle<CommaFeedConfiguration>() {
@Override
public WebConfiguration getWebConfiguration(CommaFeedConfiguration configuration) {
WebConfiguration config = new WebConfiguration();
config.getFrameOptionsHeaderFactory().setEnabled(true);
return config;
}
});
bootstrap.addBundle(new MigrationsBundle<CommaFeedConfiguration>() { bootstrap.addBundle(new MigrationsBundle<CommaFeedConfiguration>() {
@Override @Override
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) { public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
@@ -149,6 +140,8 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
@Override @Override
public void run(CommaFeedConfiguration config, Environment environment) throws Exception { public void run(CommaFeedConfiguration config, Environment environment) throws Exception {
PasswordConstraintValidator.setStrict(config.getApplicationSettings().getStrictPasswordPolicy());
// guice init // guice init
Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics())); Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics()));

View File

@@ -17,8 +17,10 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import io.dropwizard.Configuration; import io.dropwizard.Configuration;
import io.dropwizard.db.DataSourceFactory; import io.dropwizard.db.DataSourceFactory;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
@Getter @Getter
@Setter
public class CommaFeedConfiguration extends Configuration { public class CommaFeedConfiguration extends Configuration {
public enum CacheType { public enum CacheType {
@@ -56,6 +58,7 @@ public class CommaFeedConfiguration extends Configuration {
} }
@Getter @Getter
@Setter
public static class ApplicationSettings { public static class ApplicationSettings {
@NotNull @NotNull
@NotBlank @NotBlank
@@ -66,6 +69,10 @@ public class CommaFeedConfiguration extends Configuration {
@Valid @Valid
private Boolean allowRegistrations; private Boolean allowRegistrations;
@NotNull
@Valid
private Boolean strictPasswordPolicy = true;
@NotNull @NotNull
@Valid @Valid
private Boolean createDemoAccount; private Boolean createDemoAccount;
@@ -124,6 +131,10 @@ public class CommaFeedConfiguration extends Configuration {
@Valid @Valid
private Integer maxFeedCapacity; private Integer maxFeedCapacity;
@NotNull
@Valid
private Integer maxFeedsPerUser = 0;
@NotNull @NotNull
@Min(0) @Min(0)
@Valid @Valid

View File

@@ -12,7 +12,7 @@ import java.util.List;
*/ */
public class FixedSizeSortedSet<E> { public class FixedSizeSortedSet<E> {
private List<E> inner; private final List<E> inner;
private final Comparator<? super E> comparator; private final Comparator<? super E> comparator;
private final int capacity; private final int capacity;

View File

@@ -18,7 +18,7 @@ import com.querydsl.core.types.Predicate;
@Singleton @Singleton
public class FeedCategoryDAO extends GenericDAO<FeedCategory> { public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
private QFeedCategory category = QFeedCategory.feedCategory; private final QFeedCategory category = QFeedCategory.feedCategory;
@Inject @Inject
public FeedCategoryDAO(SessionFactory sessionFactory) { public FeedCategoryDAO(SessionFactory sessionFactory) {

View File

@@ -13,10 +13,8 @@ import org.hibernate.SessionFactory;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.QFeed; import com.commafeed.backend.model.QFeed;
import com.commafeed.backend.model.QFeedSubscription; import com.commafeed.backend.model.QFeedSubscription;
import com.commafeed.backend.model.QUser;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.JPQLQuery;
@Singleton @Singleton
public class FeedDAO extends GenericDAO<Feed> { public class FeedDAO extends GenericDAO<Feed> {
@@ -28,18 +26,12 @@ public class FeedDAO extends GenericDAO<Feed> {
super(sessionFactory); super(sessionFactory);
} }
public List<Feed> findNextUpdatable(int count, Date lastLoginThreshold) { public List<Feed> findNextUpdatable(int count) {
JPQLQuery<Feed> query = query().selectFrom(feed); return query().selectFrom(feed)
query.where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(new Date()))); .where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(new Date())))
.orderBy(feed.disabledUntil.asc())
if (lastLoginThreshold != null) { .limit(count)
QFeedSubscription subs = QFeedSubscription.feedSubscription; .fetch();
QUser user = QUser.user;
query.join(subs).on(subs.feed.id.eq(feed.id)).join(subs.user, user).where(user.lastLogin.gt(lastLoginThreshold));
}
return query.orderBy(feed.disabledUntil.asc()).limit(count).fetch();
} }
public void setDisabledUntil(List<Long> feedIds, Date date) { public void setDisabledUntil(List<Long> feedIds, Date date) {

View File

@@ -21,7 +21,7 @@ import lombok.Getter;
@Singleton @Singleton
public class FeedEntryDAO extends GenericDAO<FeedEntry> { public class FeedEntryDAO extends GenericDAO<FeedEntry> {
private QFeedEntry entry = QFeedEntry.feedEntry; private final QFeedEntry entry = QFeedEntry.feedEntry;
@Inject @Inject
public FeedEntryDAO(SessionFactory sessionFactory) { public FeedEntryDAO(SessionFactory sessionFactory) {

View File

@@ -77,7 +77,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) { private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
if (status == null) { if (status == null) {
Date unreadThreshold = config.getApplicationSettings().getUnreadThreshold(); Date unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
boolean read = unreadThreshold == null ? false : entry.getUpdated().before(unreadThreshold); boolean read = unreadThreshold != null && entry.getUpdated().before(unreadThreshold);
status = new FeedEntryStatus(user, sub, entry); status = new FeedEntryStatus(user, sub, entry);
status.setRead(read); status.setRead(read);
status.setMarkable(!read); status.setMarkable(!read);

View File

@@ -15,7 +15,7 @@ import com.commafeed.backend.model.User;
@Singleton @Singleton
public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> { public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
private QFeedEntryTag tag = QFeedEntryTag.feedEntryTag; private final QFeedEntryTag tag = QFeedEntryTag.feedEntryTag;
@Inject @Inject
public FeedEntryTagDAO(SessionFactory sessionFactory) { public FeedEntryTagDAO(SessionFactory sessionFactory) {

View File

@@ -21,7 +21,7 @@ import com.querydsl.jpa.JPQLQuery;
@Singleton @Singleton
public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> { public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
private QFeedSubscription sub = QFeedSubscription.feedSubscription; private final QFeedSubscription sub = QFeedSubscription.feedSubscription;
@Inject @Inject
public FeedSubscriptionDAO(SessionFactory sessionFactory) { public FeedSubscriptionDAO(SessionFactory sessionFactory) {
@@ -59,6 +59,10 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
return initRelations(subs); return initRelations(subs);
} }
public Long count(User user) {
return query().select(sub.count()).from(sub).where(sub.user.eq(user)).fetchOne();
}
public List<FeedSubscription> findByCategory(User user, FeedCategory category) { public List<FeedSubscription> findByCategory(User user, FeedCategory category) {
JPQLQuery<FeedSubscription> query = query().selectFrom(sub).where(sub.user.eq(user)); JPQLQuery<FeedSubscription> query = query().selectFrom(sub).where(sub.user.eq(user));
if (category == null) { if (category == null) {

View File

@@ -15,7 +15,7 @@ import io.dropwizard.hibernate.AbstractDAO;
public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T> { public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T> {
private JPAQueryFactory factory; private final JPAQueryFactory factory;
protected GenericDAO(SessionFactory sessionFactory) { protected GenericDAO(SessionFactory sessionFactory) {
super(sessionFactory); super(sessionFactory);

View File

@@ -1,53 +1,63 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.hibernate.Session; import org.hibernate.Session;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
import org.hibernate.Transaction; import org.hibernate.Transaction;
import org.hibernate.context.internal.ManagedSessionContext; import org.hibernate.context.internal.ManagedSessionContext;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton
public class UnitOfWork { public class UnitOfWork {
public static void run(SessionFactory sessionFactory, SessionRunner sessionRunner) { private final SessionFactory sessionFactory;
call(sessionFactory, () -> {
public void run(SessionRunner sessionRunner) {
call(() -> {
sessionRunner.runInSession(); sessionRunner.runInSession();
return null; return null;
}); });
} }
public static <T> T call(SessionFactory sessionFactory, SessionRunnerReturningValue<T> sessionRunner) { public <T> T call(SessionRunnerReturningValue<T> sessionRunner) {
final Session session = sessionFactory.openSession();
if (ManagedSessionContext.hasBind(sessionFactory)) {
throw new IllegalStateException("Already in a unit of work!");
}
T t = null; T t = null;
try {
ManagedSessionContext.bind(session); boolean sessionAlreadyBound = ManagedSessionContext.hasBind(sessionFactory);
session.beginTransaction(); try (Session session = sessionFactory.openSession()) {
if (!sessionAlreadyBound) {
ManagedSessionContext.bind(session);
}
Transaction tx = session.beginTransaction();
try { try {
t = sessionRunner.runInSession(); t = sessionRunner.runInSession();
commitTransaction(session); commitTransaction(tx);
} catch (Exception e) { } catch (Exception e) {
rollbackTransaction(session); rollbackTransaction(tx);
UnitOfWork.<RuntimeException> rethrow(e); UnitOfWork.rethrow(e);
} }
} finally { } finally {
session.close(); if (!sessionAlreadyBound) {
ManagedSessionContext.unbind(sessionFactory); ManagedSessionContext.unbind(sessionFactory);
}
} }
return t; return t;
} }
private static void rollbackTransaction(Session session) { private static void rollbackTransaction(Transaction tx) {
final Transaction txn = session.getTransaction(); if (tx != null && tx.isActive()) {
if (txn != null && txn.isActive()) { tx.rollback();
txn.rollback();
} }
} }
private static void commitTransaction(Session session) { private static void commitTransaction(Transaction tx) {
final Transaction txn = session.getTransaction(); if (tx != null && tx.isActive()) {
if (txn != null && txn.isActive()) { tx.commit();
txn.commit();
} }
} }

View File

@@ -11,7 +11,7 @@ import com.commafeed.backend.model.User;
@Singleton @Singleton
public class UserDAO extends GenericDAO<User> { public class UserDAO extends GenericDAO<User> {
private QUser user = QUser.user; private final QUser user = QUser.user;
@Inject @Inject
public UserDAO(SessionFactory sessionFactory) { public UserDAO(SessionFactory sessionFactory) {

View File

@@ -17,7 +17,7 @@ import com.commafeed.backend.model.UserRole.Role;
@Singleton @Singleton
public class UserRoleDAO extends GenericDAO<UserRole> { public class UserRoleDAO extends GenericDAO<UserRole> {
private QUserRole role = QUserRole.userRole; private final QUserRole role = QUserRole.userRole;
@Inject @Inject
public UserRoleDAO(SessionFactory sessionFactory) { public UserRoleDAO(SessionFactory sessionFactory) {

View File

@@ -12,7 +12,7 @@ import com.commafeed.backend.model.UserSettings;
@Singleton @Singleton
public class UserSettingsDAO extends GenericDAO<UserSettings> { public class UserSettingsDAO extends GenericDAO<UserSettings> {
private QUserSettings settings = QUserSettings.userSettings; private final QUserSettings settings = QUserSettings.userSettings;
@Inject @Inject
public UserSettingsDAO(SessionFactory sessionFactory) { public UserSettingsDAO(SessionFactory sessionFactory) {

View File

@@ -16,7 +16,7 @@ import lombok.RequiredArgsConstructor;
public class FeedEntryKeyword { public class FeedEntryKeyword {
public enum Mode { public enum Mode {
INCLUDE, EXCLUDE; INCLUDE, EXCLUDE
} }
private final String keyword; private final String keyword;

View File

@@ -73,7 +73,7 @@ public class FeedFetcher {
boolean etagHeaderValueChanged = !StringUtils.equals(eTag, result.getETag()); boolean etagHeaderValueChanged = !StringUtils.equals(eTag, result.getETag());
String hash = DigestUtils.sha1Hex(content); String hash = DigestUtils.sha1Hex(content);
if (lastContentHash != null && hash != null && lastContentHash.equals(hash)) { if (lastContentHash != null && lastContentHash.equals(hash)) {
log.debug("content hash not modified: {}", feedUrl); log.debug("content hash not modified: {}", feedUrl);
throw new NotModifiedException("content hash not modified", throw new NotModifiedException("content hash not modified",
lastModifiedHeaderValueChanged ? result.getLastModifiedSince() : null, lastModifiedHeaderValueChanged ? result.getLastModifiedSince() : null,

View File

@@ -16,8 +16,8 @@ import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.SessionFactory;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
@@ -33,7 +33,7 @@ import lombok.extern.slf4j.Slf4j;
@Singleton @Singleton
public class FeedRefreshEngine implements Managed { public class FeedRefreshEngine implements Managed {
private final SessionFactory sessionFactory; private final UnitOfWork unitOfWork;
private final FeedDAO feedDAO; private final FeedDAO feedDAO;
private final FeedRefreshWorker worker; private final FeedRefreshWorker worker;
private final FeedRefreshUpdater updater; private final FeedRefreshUpdater updater;
@@ -45,13 +45,13 @@ public class FeedRefreshEngine implements Managed {
private final ExecutorService feedProcessingLoopExecutor; private final ExecutorService feedProcessingLoopExecutor;
private final ExecutorService refillLoopExecutor; private final ExecutorService refillLoopExecutor;
private final ExecutorService refillExecutor; private final ExecutorService refillExecutor;
private final ExecutorService workerExecutor; private final ThreadPoolExecutor workerExecutor;
private final ExecutorService databaseUpdaterExecutor; private final ThreadPoolExecutor databaseUpdaterExecutor;
@Inject @Inject
public FeedRefreshEngine(SessionFactory sessionFactory, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater, public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater,
CommaFeedConfiguration config, MetricRegistry metrics) { CommaFeedConfiguration config, MetricRegistry metrics) {
this.sessionFactory = sessionFactory; this.unitOfWork = unitOfWork;
this.feedDAO = feedDAO; this.feedDAO = feedDAO;
this.worker = worker; this.worker = worker;
this.updater = updater; this.updater = updater;
@@ -65,6 +65,10 @@ public class FeedRefreshEngine implements Managed {
this.refillExecutor = newDiscardingSingleThreadExecutorService(); this.refillExecutor = newDiscardingSingleThreadExecutorService();
this.workerExecutor = newBlockingExecutorService(config.getApplicationSettings().getBackgroundThreads()); this.workerExecutor = newBlockingExecutorService(config.getApplicationSettings().getBackgroundThreads());
this.databaseUpdaterExecutor = newBlockingExecutorService(config.getApplicationSettings().getDatabaseUpdateThreads()); this.databaseUpdaterExecutor = newBlockingExecutorService(config.getApplicationSettings().getDatabaseUpdateThreads());
metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge<Integer>) queue::size);
metrics.register(MetricRegistry.name(getClass(), "worker", "active"), (Gauge<Integer>) workerExecutor::getActiveCount);
metrics.register(MetricRegistry.name(getClass(), "updater", "active"), (Gauge<Integer>) databaseUpdaterExecutor::getActiveCount);
} }
@Override @Override
@@ -156,8 +160,8 @@ public class FeedRefreshEngine implements Managed {
} }
private List<Feed> getNextUpdatableFeeds(int max) { private List<Feed> getNextUpdatableFeeds(int max) {
return UnitOfWork.call(sessionFactory, () -> { return unitOfWork.call(() -> {
List<Feed> feeds = feedDAO.findNextUpdatable(max, getLastLoginThreshold()); List<Feed> feeds = feedDAO.findNextUpdatable(max);
// update disabledUntil to prevent feeds from being returned again by feedDAO.findNextUpdatable() // update disabledUntil to prevent feeds from being returned again by feedDAO.findNextUpdatable()
Date nextUpdateDate = DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes()); Date nextUpdateDate = DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes());
feedDAO.setDisabledUntil(feeds.stream().map(AbstractModel::getId).collect(Collectors.toList()), nextUpdateDate); feedDAO.setDisabledUntil(feeds.stream().map(AbstractModel::getId).collect(Collectors.toList()), nextUpdateDate);
@@ -169,10 +173,6 @@ public class FeedRefreshEngine implements Managed {
return Math.min(100, 3 * config.getApplicationSettings().getBackgroundThreads()); return Math.min(100, 3 * config.getApplicationSettings().getBackgroundThreads());
} }
private Date getLastLoginThreshold() {
return Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad()) ? DateUtils.addDays(new Date(), -30) : null;
}
@Override @Override
public void stop() { public void stop() {
this.feedProcessingLoopExecutor.shutdownNow(); this.feedProcessingLoopExecutor.shutdownNow();
@@ -185,7 +185,7 @@ public class FeedRefreshEngine implements Managed {
/** /**
* returns an ExecutorService with a single thread that discards tasks if a task is already running * returns an ExecutorService with a single thread that discards tasks if a task is already running
*/ */
private ExecutorService newDiscardingSingleThreadExecutorService() { private ThreadPoolExecutor newDiscardingSingleThreadExecutorService() {
ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>()); ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
return pool; return pool;
@@ -194,7 +194,7 @@ public class FeedRefreshEngine implements Managed {
/** /**
* returns an ExecutorService that blocks submissions until a thread is available * returns an ExecutorService that blocks submissions until a thread is available
*/ */
private ExecutorService newBlockingExecutorService(int threads) { private ThreadPoolExecutor newBlockingExecutorService(int threads) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>()); ThreadPoolExecutor pool = new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
pool.setRejectedExecutionHandler((r, e) -> { pool.setRejectedExecutionHandler((r, e) -> {
if (e.isShutdown()) { if (e.isShutdown()) {

View File

@@ -13,8 +13,8 @@ import com.commafeed.backend.model.Feed;
@Singleton @Singleton
public class FeedRefreshIntervalCalculator { public class FeedRefreshIntervalCalculator {
private boolean heavyLoad; private final boolean heavyLoad;
private int refreshIntervalMinutes; private final int refreshIntervalMinutes;
@Inject @Inject
public FeedRefreshIntervalCalculator(CommaFeedConfiguration config) { public FeedRefreshIntervalCalculator(CommaFeedConfiguration config) {

View File

@@ -15,7 +15,6 @@ import javax.inject.Singleton;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.SessionFactory;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
@@ -46,7 +45,7 @@ import lombok.extern.slf4j.Slf4j;
@Singleton @Singleton
public class FeedRefreshUpdater implements Managed { public class FeedRefreshUpdater implements Managed {
private final SessionFactory sessionFactory; private final UnitOfWork unitOfWork;
private final FeedService feedService; private final FeedService feedService;
private final FeedEntryService feedEntryService; private final FeedEntryService feedEntryService;
private final PubSubService pubSubService; private final PubSubService pubSubService;
@@ -63,10 +62,10 @@ public class FeedRefreshUpdater implements Managed {
private final Meter entryInserted; private final Meter entryInserted;
@Inject @Inject
public FeedRefreshUpdater(SessionFactory sessionFactory, FeedService feedService, FeedEntryService feedEntryService, public FeedRefreshUpdater(UnitOfWork unitOfWork, FeedService feedService, FeedEntryService feedEntryService,
PubSubService pubSubService, CommaFeedConfiguration config, MetricRegistry metrics, FeedSubscriptionDAO feedSubscriptionDAO, PubSubService pubSubService, CommaFeedConfiguration config, MetricRegistry metrics, FeedSubscriptionDAO feedSubscriptionDAO,
CacheService cache, WebSocketSessions webSocketSessions) { CacheService cache, WebSocketSessions webSocketSessions) {
this.sessionFactory = sessionFactory; this.unitOfWork = unitOfWork;
this.feedService = feedService; this.feedService = feedService;
this.feedEntryService = feedEntryService; this.feedEntryService = feedEntryService;
this.pubSubService = pubSubService; this.pubSubService = pubSubService;
@@ -89,7 +88,7 @@ public class FeedRefreshUpdater implements Managed {
// lock on feed, make sure we are not updating the same feed twice at // lock on feed, make sure we are not updating the same feed twice at
// the same time // the same time
String key1 = StringUtils.trimToEmpty("" + feed.getId()); String key1 = StringUtils.trimToEmpty(String.valueOf(feed.getId()));
// lock on content, make sure we are not updating the same entry // lock on content, make sure we are not updating the same entry
// twice at the same time // twice at the same time
@@ -107,7 +106,7 @@ public class FeedRefreshUpdater implements Managed {
locked2 = lock2.tryLock(1, TimeUnit.MINUTES); locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
if (locked1 && locked2) { if (locked1 && locked2) {
processed = true; processed = true;
inserted = UnitOfWork.call(sessionFactory, () -> feedEntryService.addEntry(feed, entry, subscriptions)); inserted = unitOfWork.call(() -> feedEntryService.addEntry(feed, entry, subscriptions));
if (inserted) { if (inserted) {
entryInserted.mark(); entryInserted.mark();
} }
@@ -164,7 +163,7 @@ public class FeedRefreshUpdater implements Managed {
if (!lastEntries.contains(cacheKey)) { if (!lastEntries.contains(cacheKey)) {
log.debug("cache miss for {}", entry.getUrl()); log.debug("cache miss for {}", entry.getUrl());
if (subscriptions == null) { if (subscriptions == null) {
subscriptions = UnitOfWork.call(sessionFactory, () -> feedSubscriptionDAO.findByFeed(feed)); subscriptions = unitOfWork.call(() -> feedSubscriptionDAO.findByFeed(feed));
} }
AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions); AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
processed &= addEntryResult.processed; processed &= addEntryResult.processed;
@@ -204,7 +203,7 @@ public class FeedRefreshUpdater implements Managed {
feedUpdated.mark(); feedUpdated.mark();
} }
UnitOfWork.run(sessionFactory, () -> feedService.save(feed)); unitOfWork.run(() -> feedService.save(feed));
return processed; return processed;
} }

View File

@@ -154,7 +154,7 @@ public class FeedUtils {
for (Emit emit : emits) { for (Emit emit : emits) {
int matchIndex = emit.getStart(); int matchIndex = emit.getStart();
sb.append(source.substring(prevIndex, matchIndex)); sb.append(source, prevIndex, matchIndex);
sb.append(HtmlEntities.HTML_TO_NUMERIC_MAP.get(emit.getKeyword())); sb.append(HtmlEntities.HTML_TO_NUMERIC_MAP.get(emit.getKeyword()));
prevIndex = emit.getEnd() + 1; prevIndex = emit.getEnd() + 1;
} }
@@ -228,7 +228,7 @@ public class FeedUtils {
if (index == -1) { if (index == -1) {
return null; return null;
} }
String encoding = pi.substring(index + 10, pi.length()); String encoding = pi.substring(index + 10);
encoding = encoding.substring(0, encoding.indexOf('"')); encoding = encoding.substring(0, encoding.indexOf('"'));
return encoding; return encoding;
} }

View File

@@ -15,7 +15,6 @@ import com.commafeed.backend.feed.FeedUtils;
import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.service.FeedSubscriptionService; import com.commafeed.backend.service.FeedSubscriptionService;
import com.commafeed.backend.service.FeedSubscriptionService.FeedSubscriptionException;
import com.rometools.opml.feed.opml.Opml; import com.rometools.opml.feed.opml.Opml;
import com.rometools.opml.feed.opml.Outline; import com.rometools.opml.feed.opml.Outline;
import com.rometools.rome.io.FeedException; import com.rometools.rome.io.FeedException;
@@ -78,8 +77,6 @@ public class OPMLImporter {
// make sure we continue with the import process even if a feed failed // make sure we continue with the import process even if a feed failed
try { try {
feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent, position); feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent, position);
} catch (FeedSubscriptionException e) {
throw e;
} catch (Exception e) { } catch (Exception e) {
log.error("error while importing {}: {}", outline.getXmlUrl(), e.getMessage()); log.error("error while importing {}: {}", outline.getXmlUrl(), e.getMessage());
} }

View File

@@ -23,11 +23,7 @@ public class OPML11Parser extends OPML10Parser {
public boolean isMyType(Document document) { public boolean isMyType(Document document) {
Element e = document.getRootElement(); Element e = document.getRootElement();
if (e.getName().equals("opml")) { return e.getName().equals("opml");
return true;
}
return false;
} }

View File

@@ -6,8 +6,6 @@ import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.hibernate.SessionFactory;
import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedEntryContentDAO; import com.commafeed.backend.dao.FeedEntryContentDAO;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
@@ -30,7 +28,7 @@ public class DatabaseCleaningService {
private static final int BATCH_SIZE = 100; private static final int BATCH_SIZE = 100;
private final SessionFactory sessionFactory; private final UnitOfWork unitOfWork;
private final FeedDAO feedDAO; private final FeedDAO feedDAO;
private final FeedEntryDAO feedEntryDAO; private final FeedEntryDAO feedEntryDAO;
private final FeedEntryContentDAO feedEntryContentDAO; private final FeedEntryContentDAO feedEntryContentDAO;
@@ -42,16 +40,16 @@ public class DatabaseCleaningService {
int deleted = 0; int deleted = 0;
long entriesTotal = 0; long entriesTotal = 0;
do { do {
List<Feed> feeds = UnitOfWork.call(sessionFactory, () -> feedDAO.findWithoutSubscriptions(1)); List<Feed> feeds = unitOfWork.call(() -> feedDAO.findWithoutSubscriptions(1));
for (Feed feed : feeds) { for (Feed feed : feeds) {
int entriesDeleted = 0; int entriesDeleted = 0;
do { do {
entriesDeleted = UnitOfWork.call(sessionFactory, () -> feedEntryDAO.delete(feed.getId(), BATCH_SIZE)); entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), BATCH_SIZE));
entriesTotal += entriesDeleted; entriesTotal += entriesDeleted;
log.info("removed {} entries for feeds without subscriptions", entriesTotal); log.info("removed {} entries for feeds without subscriptions", entriesTotal);
} while (entriesDeleted > 0); } while (entriesDeleted > 0);
} }
deleted = UnitOfWork.call(sessionFactory, () -> feedDAO.delete(feeds)); deleted = unitOfWork.call(() -> feedDAO.delete(feeds));
total += deleted; total += deleted;
log.info("removed {} feeds without subscriptions", total); log.info("removed {} feeds without subscriptions", total);
} while (deleted != 0); } while (deleted != 0);
@@ -64,7 +62,7 @@ public class DatabaseCleaningService {
long total = 0; long total = 0;
int deleted = 0; int deleted = 0;
do { do {
deleted = UnitOfWork.call(sessionFactory, () -> feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE)); deleted = unitOfWork.call(() -> feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE));
total += deleted; total += deleted;
log.info("removed {} contents without entries", total); log.info("removed {} contents without entries", total);
} while (deleted != 0); } while (deleted != 0);
@@ -75,8 +73,7 @@ public class DatabaseCleaningService {
public long cleanEntriesForFeedsExceedingCapacity(final int maxFeedCapacity) { public long cleanEntriesForFeedsExceedingCapacity(final int maxFeedCapacity) {
long total = 0; long total = 0;
while (true) { while (true) {
List<FeedCapacity> feeds = UnitOfWork.call(sessionFactory, List<FeedCapacity> feeds = unitOfWork.call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, BATCH_SIZE));
() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, BATCH_SIZE));
if (feeds.isEmpty()) { if (feeds.isEmpty()) {
break; break;
} }
@@ -85,8 +82,7 @@ public class DatabaseCleaningService {
long remaining = feed.getCapacity() - maxFeedCapacity; long remaining = feed.getCapacity() - maxFeedCapacity;
do { do {
final long rem = remaining; final long rem = remaining;
int deleted = UnitOfWork.call(sessionFactory, int deleted = unitOfWork.call(() -> feedEntryDAO.deleteOldEntries(feed.getId(), Math.min(BATCH_SIZE, rem)));
() -> feedEntryDAO.deleteOldEntries(feed.getId(), Math.min(BATCH_SIZE, rem)));
total += deleted; total += deleted;
remaining -= deleted; remaining -= deleted;
log.info("removed {} entries for feeds exceeding capacity", total); log.info("removed {} entries for feeds exceeding capacity", total);
@@ -102,8 +98,7 @@ public class DatabaseCleaningService {
long total = 0; long total = 0;
int deleted = 0; int deleted = 0;
do { do {
deleted = UnitOfWork.call(sessionFactory, deleted = unitOfWork.call(() -> feedEntryStatusDAO.delete(feedEntryStatusDAO.getOldStatuses(olderThan, BATCH_SIZE)));
() -> feedEntryStatusDAO.delete(feedEntryStatusDAO.getOldStatuses(olderThan, BATCH_SIZE)));
total += deleted; total += deleted;
log.info("removed {} old read statuses", total); log.info("removed {} old read statuses", total);
} while (deleted != 0); } while (deleted != 0);

View File

@@ -27,6 +27,7 @@ import lombok.extern.slf4j.Slf4j;
@Singleton @Singleton
public class DatabaseStartupService implements Managed { public class DatabaseStartupService implements Managed {
private final UnitOfWork unitOfWork;
private final SessionFactory sessionFactory; private final SessionFactory sessionFactory;
private final UserDAO userDAO; private final UserDAO userDAO;
private final UserService userService; private final UserService userService;
@@ -35,9 +36,9 @@ public class DatabaseStartupService implements Managed {
@Override @Override
public void start() { public void start() {
updateSchema(); updateSchema();
long count = UnitOfWork.call(sessionFactory, userDAO::count); long count = unitOfWork.call(userDAO::count);
if (count == 0) { if (count == 0) {
UnitOfWork.run(sessionFactory, this::initialData); unitOfWork.run(this::initialData);
} }
} }

View File

@@ -22,7 +22,7 @@ public class FeedService {
private final FeedDAO feedDAO; private final FeedDAO feedDAO;
private final Set<AbstractFaviconFetcher> faviconFetchers; private final Set<AbstractFaviconFetcher> faviconFetchers;
private Favicon defaultFavicon; private final Favicon defaultFavicon;
@Inject @Inject
public FeedService(FeedDAO feedDAO, Set<AbstractFaviconFetcher> faviconFetchers) { public FeedService(FeedDAO feedDAO, Set<AbstractFaviconFetcher> faviconFetchers) {

View File

@@ -57,6 +57,13 @@ public class FeedSubscriptionService {
throw new FeedSubscriptionException("Could not subscribe to a feed from this CommaFeed instance"); throw new FeedSubscriptionException("Could not subscribe to a feed from this CommaFeed instance");
} }
Integer maxFeedsPerUser = config.getApplicationSettings().getMaxFeedsPerUser();
if (maxFeedsPerUser > 0 && feedSubscriptionDAO.count(user) >= maxFeedsPerUser) {
String message = String.format("You cannot subscribe to more feeds on this CommaFeed instance (max %s feeds per user)",
maxFeedsPerUser);
throw new FeedSubscriptionException(message);
}
Feed feed = feedService.findOrCreate(url); Feed feed = feedService.findOrCreate(url);
// upgrade feed to https if it was using http // upgrade feed to https if it was using http

View File

@@ -41,9 +41,9 @@ public class MailService {
Properties props = new Properties(); Properties props = new Properties();
props.put("mail.smtp.auth", "true"); props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "" + settings.isSmtpTls()); props.put("mail.smtp.starttls.enable", String.valueOf(settings.isSmtpTls()));
props.put("mail.smtp.host", settings.getSmtpHost()); props.put("mail.smtp.host", settings.getSmtpHost());
props.put("mail.smtp.port", "" + settings.getSmtpPort()); props.put("mail.smtp.port", String.valueOf(settings.getSmtpPort()));
Session session = Session.getInstance(props, new Authenticator() { Session session = Session.getInstance(props, new Authenticator() {
@Override @Override

View File

@@ -17,7 +17,6 @@ import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils; import org.apache.http.util.EntityUtils;
import org.hibernate.SessionFactory;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter;
@@ -40,7 +39,7 @@ public class PubSubService {
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
private final FeedService feedService; private final FeedService feedService;
private final SessionFactory sessionFactory; private final UnitOfWork unitOfWork;
public void subscribe(Feed feed) { public void subscribe(Feed feed) {
String hub = feed.getPushHub(); String hub = feed.getPushHub();
@@ -75,7 +74,7 @@ public class PubSubService {
if (code == 400 && StringUtils.contains(message, pushpressError)) { if (code == 400 && StringUtils.contains(message, pushpressError)) {
String[] tokens = message.split(" "); String[] tokens = message.split(" ");
feed.setPushTopic(tokens[tokens.length - 1]); feed.setPushTopic(tokens[tokens.length - 1]);
UnitOfWork.run(sessionFactory, () -> feedService.save(feed)); unitOfWork.run(() -> feedService.save(feed));
log.debug("handled pushpress subfeed {} : {}", topic, feed.getPushTopic()); log.debug("handled pushpress subfeed {} : {}", topic, feed.getPushTopic());
} else { } else {
throw new Exception( throw new Exception(

View File

@@ -2,6 +2,7 @@ package com.commafeed.backend.service;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@@ -123,7 +124,7 @@ public class UserService {
} }
public void createDemoUser() { public void createDemoUser() {
register(CommaFeedApplication.USERNAME_DEMO, "demo", "demo@commafeed.com", Arrays.asList(Role.USER), true); register(CommaFeedApplication.USERNAME_DEMO, "demo", "demo@commafeed.com", Collections.singletonList(Role.USER), true);
} }
public void unregister(User user) { public void unregister(User user) {

View File

@@ -8,6 +8,7 @@ import javax.inject.Singleton;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.service.FeedSubscriptionService; import com.commafeed.backend.service.FeedSubscriptionService;
@@ -20,6 +21,7 @@ public class PostLoginActivities {
private final UserDAO userDAO; private final UserDAO userDAO;
private final FeedSubscriptionService feedSubscriptionService; private final FeedSubscriptionService feedSubscriptionService;
private final UnitOfWork unitOfWork;
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
public void executeFor(User user) { public void executeFor(User user) {
@@ -27,19 +29,26 @@ public class PostLoginActivities {
Date now = new Date(); Date now = new Date();
boolean saveUser = false; boolean saveUser = false;
// only update lastLogin field every hour in order to not // only update lastLogin field every hour in order to not
// invalidate the cache every time someone logs in // invalidate the cache every time someone logs in
if (lastLogin == null || lastLogin.before(DateUtils.addHours(now, -1))) { if (lastLogin == null || lastLogin.before(DateUtils.addHours(now, -1))) {
user.setLastLogin(now); user.setLastLogin(now);
saveUser = true; saveUser = true;
} }
if (config.getApplicationSettings().getHeavyLoad() && user.shouldRefreshFeedsAt(now)) {
if (Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad()) && user.shouldRefreshFeedsAt(now)) {
feedSubscriptionService.refreshAll(user); feedSubscriptionService.refreshAll(user);
user.setLastFullRefresh(now); user.setLastFullRefresh(now);
saveUser = true; saveUser = true;
} }
if (saveUser) { if (saveUser) {
userDAO.saveOrUpdate(user); // Post login activites are susceptible to run for any webservice call.
// We update the user in a new transaction to update the user immediately.
// If we didn't and the webservice call takes time, subsequent webservice calls would have to wait for the first call to
// finish even if they didn't use the same database tables, because they updated the user too.
unitOfWork.run(() -> userDAO.saveOrUpdate(user));
} }
} }

View File

@@ -5,8 +5,6 @@ import java.util.concurrent.TimeUnit;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.hibernate.SessionFactory;
import com.commafeed.CommaFeedApplication; import com.commafeed.CommaFeedApplication;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UnitOfWork;
@@ -23,7 +21,7 @@ import lombok.extern.slf4j.Slf4j;
public class DemoAccountCleanupTask extends ScheduledTask { public class DemoAccountCleanupTask extends ScheduledTask {
private final CommaFeedConfiguration config; private final CommaFeedConfiguration config;
private final SessionFactory sessionFactory; private final UnitOfWork unitOfWork;
private final UserDAO userDAO; private final UserDAO userDAO;
private final UserService userService; private final UserService userService;
@@ -34,7 +32,7 @@ public class DemoAccountCleanupTask extends ScheduledTask {
} }
log.info("recreating demo user account"); log.info("recreating demo user account");
UnitOfWork.run(sessionFactory, () -> { unitOfWork.run(() -> {
User demoUser = userDAO.findByName(CommaFeedApplication.USERNAME_DEMO); User demoUser = userDAO.findByName(CommaFeedApplication.USERNAME_DEMO);
if (demoUser == null) { if (demoUser == null) {
return; return;

View File

@@ -14,8 +14,13 @@ import org.passay.PasswordValidator;
import org.passay.RuleResult; import org.passay.RuleResult;
import org.passay.WhitespaceRule; import org.passay.WhitespaceRule;
import lombok.Setter;
public class PasswordConstraintValidator implements ConstraintValidator<ValidPassword, String> { public class PasswordConstraintValidator implements ConstraintValidator<ValidPassword, String> {
@Setter
private static boolean strict = true;
@Override @Override
public void initialize(ValidPassword constraintAnnotation) { public void initialize(ValidPassword constraintAnnotation) {
// nothing to do // nothing to do
@@ -27,7 +32,7 @@ public class PasswordConstraintValidator implements ConstraintValidator<ValidPas
return true; return true;
} }
PasswordValidator validator = buildPasswordValidator(); PasswordValidator validator = strict ? buildStrictPasswordValidator() : buildLoosePasswordValidator();
RuleResult result = validator.validate(new PasswordData(value)); RuleResult result = validator.validate(new PasswordData(value));
if (result.isValid()) { if (result.isValid()) {
@@ -40,10 +45,10 @@ public class PasswordConstraintValidator implements ConstraintValidator<ValidPas
return false; return false;
} }
private PasswordValidator buildPasswordValidator() { private PasswordValidator buildStrictPasswordValidator() {
return new PasswordValidator( return new PasswordValidator(
// length // length
new LengthRule(8, 128), new LengthRule(8, 256),
// 1 uppercase char // 1 uppercase char
new CharacterRule(EnglishCharacterData.UpperCase, 1), new CharacterRule(EnglishCharacterData.UpperCase, 1),
// 1 lowercase char // 1 lowercase char
@@ -56,4 +61,12 @@ public class PasswordConstraintValidator implements ConstraintValidator<ValidPas
new WhitespaceRule()); new WhitespaceRule());
} }
private PasswordValidator buildLoosePasswordValidator() {
return new PasswordValidator(
// length
new LengthRule(6, 256),
// no whitespace
new WhitespaceRule());
}
} }

View File

@@ -21,8 +21,8 @@ import lombok.RequiredArgsConstructor;
@Singleton @Singleton
public class SecurityCheckFactoryProvider extends AbstractValueParamProvider { public class SecurityCheckFactoryProvider extends AbstractValueParamProvider {
private UserService userService; private final UserService userService;
private HttpServletRequest request; private final HttpServletRequest request;
@Inject @Inject
public SecurityCheckFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, UserService userService, public SecurityCheckFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, UserService userService,

View File

@@ -31,7 +31,7 @@ public class Category implements Serializable {
@ApiModelProperty(value = "category feeds", required = true) @ApiModelProperty(value = "category feeds", required = true)
private List<Subscription> feeds = new ArrayList<>(); private List<Subscription> feeds = new ArrayList<>();
@ApiModelProperty(value = "wether the category is expanded or collapsed", required = true) @ApiModelProperty(value = "whether the category is expanded or collapsed", required = true)
private boolean expanded; private boolean expanded;
@ApiModelProperty(value = "position of the category in the list", required = true) @ApiModelProperty(value = "position of the category in the list", required = true)

View File

@@ -1,7 +1,7 @@
package com.commafeed.frontend.model; package com.commafeed.frontend.model;
import java.io.Serializable; import java.io.Serializable;
import java.util.Arrays; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -45,7 +45,7 @@ public class Entry implements Serializable {
@ApiModelProperty(value = "comma-separated list of categories") @ApiModelProperty(value = "comma-separated list of categories")
private String categories; private String categories;
@ApiModelProperty(value = "wether entry content and title are rtl", required = true) @ApiModelProperty(value = "whether entry content and title are rtl", required = true)
private boolean rtl; private boolean rtl;
@ApiModelProperty(value = "entry author") @ApiModelProperty(value = "entry author")
@@ -99,7 +99,7 @@ public class Entry implements Serializable {
@ApiModelProperty(value = "starred status", required = true) @ApiModelProperty(value = "starred status", required = true)
private boolean starred; private boolean starred;
@ApiModelProperty(value = "wether the entry is still markable (old entry statuses are discarded)", required = true) @ApiModelProperty(value = "whether the entry is still markable (old entry statuses are discarded)", required = true)
private boolean markable; private boolean markable;
@ApiModelProperty(value = "tags", required = true) @ApiModelProperty(value = "tags", required = true)
@@ -158,13 +158,13 @@ public class Entry implements Serializable {
SyndContentImpl content = new SyndContentImpl(); SyndContentImpl content = new SyndContentImpl();
content.setValue(getContent()); content.setValue(getContent());
entry.setContents(Arrays.<SyndContent> asList(content)); entry.setContents(Collections.<SyndContent> singletonList(content));
if (getEnclosureUrl() != null) { if (getEnclosureUrl() != null) {
SyndEnclosureImpl enclosure = new SyndEnclosureImpl(); SyndEnclosureImpl enclosure = new SyndEnclosureImpl();
enclosure.setType(getEnclosureType()); enclosure.setType(getEnclosureType());
enclosure.setUrl(getEnclosureUrl()); enclosure.setUrl(getEnclosureUrl());
entry.setEnclosures(Arrays.<SyndEnclosure> asList(enclosure)); entry.setEnclosures(Collections.<SyndEnclosure> singletonList(enclosure));
} }
entry.setLink(getUrl()); entry.setLink(getUrl());

View File

@@ -104,9 +104,7 @@ public class CategoryREST {
@ApiParam(value = "only entries newer than this") @QueryParam("newerThan") Long newerThan, @ApiParam(value = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, @ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
@ApiParam(value = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit, @ApiParam(value = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit,
@ApiParam( @ApiParam(value = "ordering", allowableValues = "asc,desc") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
value = "ordering",
allowableValues = "asc,desc,abc,zyx") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
@ApiParam( @ApiParam(
value = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords, value = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords,
@ApiParam(value = "return only entry ids") @DefaultValue("false") @QueryParam("onlyIds") boolean onlyIds, @ApiParam(value = "return only entry ids") @DefaultValue("false") @QueryParam("onlyIds") boolean onlyIds,

View File

@@ -3,7 +3,7 @@ package com.commafeed.frontend.resource;
import java.io.InputStream; import java.io.InputStream;
import java.io.StringWriter; import java.io.StringWriter;
import java.net.URI; import java.net.URI;
import java.util.Arrays; import java.nio.charset.StandardCharsets;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
@@ -141,9 +141,7 @@ public class FeedREST {
@ApiParam(value = "only entries newer than this") @QueryParam("newerThan") Long newerThan, @ApiParam(value = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset, @ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
@ApiParam(value = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit, @ApiParam(value = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit,
@ApiParam( @ApiParam(value = "ordering", allowableValues = "asc,desc") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
value = "ordering",
allowableValues = "asc,desc,abc,zyx") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
@ApiParam( @ApiParam(
value = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords, value = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords,
@ApiParam(value = "return only entry ids") @DefaultValue("false") @QueryParam("onlyIds") boolean onlyIds) { @ApiParam(value = "return only entry ids") @DefaultValue("false") @QueryParam("onlyIds") boolean onlyIds) {
@@ -173,7 +171,7 @@ public class FeedREST {
entries.setErrorCount(subscription.getFeed().getErrorCount()); entries.setErrorCount(subscription.getFeed().getErrorCount());
entries.setFeedLink(subscription.getFeed().getLink()); entries.setFeedLink(subscription.getFeed().getLink());
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, Arrays.asList(subscription), unreadOnly, List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, Collections.singletonList(subscription), unreadOnly,
entryKeywords, newerThanDate, offset, limit + 1, order, true, onlyIds, null); entryKeywords, newerThanDate, offset, limit + 1, order, true, onlyIds, null);
for (FeedEntryStatus status : list) { for (FeedEntryStatus status : list) {
@@ -325,7 +323,7 @@ public class FeedREST {
FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(req.getId())); FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(req.getId()));
if (subscription != null) { if (subscription != null) {
feedEntryService.markSubscriptionEntries(user, Arrays.asList(subscription), olderThan, entryKeywords); feedEntryService.markSubscriptionEntries(user, Collections.singletonList(subscription), olderThan, entryKeywords);
} }
return Response.ok().build(); return Response.ok().build();
} }
@@ -524,7 +522,7 @@ public class FeedREST {
return Response.status(Status.FORBIDDEN).entity("Import is disabled for the demo account").build(); return Response.status(Status.FORBIDDEN).entity("Import is disabled for the demo account").build();
} }
try { try {
String opml = IOUtils.toString(input, "UTF-8"); String opml = IOUtils.toString(input, StandardCharsets.UTF_8);
opmlImporter.importOpml(user, opml); opmlImporter.importOpml(user, opml);
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);

View File

@@ -1,6 +1,6 @@
package com.commafeed.frontend.resource; package com.commafeed.frontend.resource;
import java.util.Arrays; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@@ -227,7 +227,8 @@ public class UserREST {
public Response registerUser(@Valid @ApiParam(required = true) RegistrationRequest req, public Response registerUser(@Valid @ApiParam(required = true) RegistrationRequest req,
@Context @ApiParam(hidden = true) SessionHelper sessionHelper) { @Context @ApiParam(hidden = true) SessionHelper sessionHelper) {
try { try {
User registeredUser = userService.register(req.getName(), req.getPassword(), req.getEmail(), Arrays.asList(Role.USER)); User registeredUser = userService.register(req.getName(), req.getPassword(), req.getEmail(),
Collections.singletonList(Role.USER));
userService.login(req.getName(), req.getPassword()); userService.login(req.getName(), req.getPassword());
sessionHelper.setLoggedInUser(registeredUser); sessionHelper.setLoggedInUser(registeredUser);
return Response.ok().build(); return Response.ok().build();

View File

@@ -7,8 +7,6 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.hibernate.SessionFactory;
import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserSettingsDAO; import com.commafeed.backend.dao.UserSettingsDAO;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
@@ -22,7 +20,7 @@ abstract class AbstractCustomCodeServlet extends HttpServlet {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private final SessionFactory sessionFactory; private final UnitOfWork unitOfWork;
private final UserSettingsDAO userSettingsDAO; private final UserSettingsDAO userSettingsDAO;
@Override @Override
@@ -34,7 +32,7 @@ abstract class AbstractCustomCodeServlet extends HttpServlet {
return; return;
} }
UserSettings settings = UnitOfWork.call(sessionFactory, () -> userSettingsDAO.findByUser(user.get())); UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(user.get()));
if (settings == null) { if (settings == null) {
return; return;
} }

View File

@@ -2,8 +2,7 @@ package com.commafeed.frontend.servlet;
import javax.inject.Inject; import javax.inject.Inject;
import org.hibernate.SessionFactory; import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserSettingsDAO; import com.commafeed.backend.dao.UserSettingsDAO;
import com.commafeed.backend.model.UserSettings; import com.commafeed.backend.model.UserSettings;
@@ -12,8 +11,8 @@ public class CustomCssServlet extends AbstractCustomCodeServlet {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Inject @Inject
public CustomCssServlet(SessionFactory sessionFactory, UserSettingsDAO userSettingsDAO) { public CustomCssServlet(UnitOfWork unitOfWork, UserSettingsDAO userSettingsDAO) {
super(sessionFactory, userSettingsDAO); super(unitOfWork, userSettingsDAO);
} }
@Override @Override

View File

@@ -3,8 +3,7 @@ package com.commafeed.frontend.servlet;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.hibernate.SessionFactory; import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserSettingsDAO; import com.commafeed.backend.dao.UserSettingsDAO;
import com.commafeed.backend.model.UserSettings; import com.commafeed.backend.model.UserSettings;
@@ -14,8 +13,8 @@ public class CustomJsServlet extends AbstractCustomCodeServlet {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Inject @Inject
public CustomJsServlet(SessionFactory sessionFactory, UserSettingsDAO userSettingsDAO) { public CustomJsServlet(UnitOfWork unitOfWork, UserSettingsDAO userSettingsDAO) {
super(sessionFactory, userSettingsDAO); super(unitOfWork, userSettingsDAO);
} }
@Override @Override

View File

@@ -12,7 +12,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.hibernate.SessionFactory;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedCategoryDAO;
@@ -39,7 +38,7 @@ public class NextUnreadServlet extends HttpServlet {
private static final String PARAM_CATEGORYID = "category"; private static final String PARAM_CATEGORYID = "category";
private static final String PARAM_READINGORDER = "order"; private static final String PARAM_READINGORDER = "order";
private final SessionFactory sessionFactory; private final UnitOfWork unitOfWork;
private final FeedSubscriptionDAO feedSubscriptionDAO; private final FeedSubscriptionDAO feedSubscriptionDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO; private final FeedEntryStatusDAO feedEntryStatusDAO;
private final FeedCategoryDAO feedCategoryDAO; private final FeedCategoryDAO feedCategoryDAO;
@@ -54,7 +53,7 @@ public class NextUnreadServlet extends HttpServlet {
SessionHelper sessionHelper = new SessionHelper(req); SessionHelper sessionHelper = new SessionHelper(req);
Optional<User> user = sessionHelper.getLoggedInUser(); Optional<User> user = sessionHelper.getLoggedInUser();
if (user.isPresent()) { if (user.isPresent()) {
UnitOfWork.run(sessionFactory, () -> userService.performPostLoginActivities(user.get())); unitOfWork.run(() -> userService.performPostLoginActivities(user.get()));
} }
if (!user.isPresent()) { if (!user.isPresent()) {
resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl())); resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl()));
@@ -63,7 +62,7 @@ public class NextUnreadServlet extends HttpServlet {
final ReadingOrder order = StringUtils.equals(orderParam, "asc") ? ReadingOrder.asc : ReadingOrder.desc; final ReadingOrder order = StringUtils.equals(orderParam, "asc") ? ReadingOrder.asc : ReadingOrder.desc;
FeedEntryStatus status = UnitOfWork.call(sessionFactory, () -> { FeedEntryStatus status = unitOfWork.call(() -> {
FeedEntryStatus s = null; FeedEntryStatus s = null;
if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) { if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user.get()); List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user.get());

View File

@@ -17,7 +17,7 @@ import org.glassfish.jersey.server.spi.internal.ValueParamProvider;
@Singleton @Singleton
public class SessionHelperFactoryProvider extends AbstractValueParamProvider { public class SessionHelperFactoryProvider extends AbstractValueParamProvider {
private HttpServletRequest request; private final HttpServletRequest request;
@Inject @Inject
public SessionHelperFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, HttpServletRequest request) { public SessionHelperFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, HttpServletRequest request) {

View File

@@ -7,7 +7,7 @@ import lombok.experimental.UtilityClass;
@UtilityClass @UtilityClass
public class WebSocketMessageBuilder { public class WebSocketMessageBuilder {
public static final String newFeedEntries(FeedSubscription subscription) { public static String newFeedEntries(FeedSubscription subscription) {
return String.format("%s:%s", "new-feed-entries", subscription.getId()); return String.format("%s:%s", "new-feed-entries", subscription.getId());
} }

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet id="new-hibernate-id-generator" author="athou">
<modifyDataType tableName="hibernate_sequences" columnName="sequence_next_hi_value" newDataType="BIGINT" />
<sql>update hibernate_sequences
set sequence_next_hi_value=
(select coalesce(max(id), 0) + 1 from FEEDCATEGORIES)
where sequence_name = 'FEEDCATEGORIES'</sql>
<sql>update hibernate_sequences
set sequence_next_hi_value=
(select coalesce(max(id), 0) + 1 from FEEDENTRIES)
where sequence_name = 'FEEDENTRIES'</sql>
<sql>update hibernate_sequences
set sequence_next_hi_value=
(select coalesce(max(id), 0) + 1 from FEEDENTRYCONTENTS)
where sequence_name = 'FEEDENTRYCONTENTS'</sql>
<sql>update hibernate_sequences
set sequence_next_hi_value=
(select coalesce(max(id), 0) + 1 from FEEDENTRYSTATUSES)
where sequence_name = 'FEEDENTRYSTATUSES'</sql>
<sql>update hibernate_sequences
set sequence_next_hi_value=
(select coalesce(max(id), 0) + 1 from FEEDS)
where sequence_name = 'FEEDS'</sql>
<sql>update hibernate_sequences
set sequence_next_hi_value=
(select coalesce(max(id), 0) + 1 from FEEDSUBSCRIPTIONS)
where sequence_name = 'FEEDSUBSCRIPTIONS'</sql>
<sql>update hibernate_sequences
set sequence_next_hi_value=
(select coalesce(max(id), 0) + 1 from USERROLES)
where sequence_name = 'USERROLES'</sql>
<sql>update hibernate_sequences
set sequence_next_hi_value=
(select coalesce(max(id), 0) + 1 from USERS)
where sequence_name = 'USERS'</sql>
<sql>update hibernate_sequences
set sequence_next_hi_value=
(select coalesce(max(id), 0) + 1 from USERSETTINGS)
where sequence_name = 'USERSETTINGS'</sql>
</changeSet>
</databaseChangeLog>

View File

@@ -3,20 +3,21 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"> xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<property name="blob_type" value="bytea" dbms="postgresql"/> <property name="blob_type" value="bytea" dbms="postgresql" />
<property name="blob_type" value="blob" dbms="h2"/> <property name="blob_type" value="blob" dbms="h2" />
<property name="blob_type" value="blob" dbms="mysql,mariadb"/> <property name="blob_type" value="blob" dbms="mysql,mariadb" />
<property name="blob_type" value="blob" dbms="mssql"/> <property name="blob_type" value="blob" dbms="mssql" />
<include file="changelogs/db.changelog-1.0.xml"/> <include file="changelogs/db.changelog-1.0.xml" />
<include file="changelogs/db.changelog-1.1.xml"/> <include file="changelogs/db.changelog-1.1.xml" />
<include file="changelogs/db.changelog-1.2.xml"/> <include file="changelogs/db.changelog-1.2.xml" />
<include file="changelogs/db.changelog-1.3.xml"/> <include file="changelogs/db.changelog-1.3.xml" />
<include file="changelogs/db.changelog-1.4.xml"/> <include file="changelogs/db.changelog-1.4.xml" />
<include file="changelogs/db.changelog-1.5.xml"/> <include file="changelogs/db.changelog-1.5.xml" />
<include file="changelogs/db.changelog-2.1.xml"/> <include file="changelogs/db.changelog-2.1.xml" />
<include file="changelogs/db.changelog-2.2.xml"/> <include file="changelogs/db.changelog-2.2.xml" />
<include file="changelogs/db.changelog-2.6.xml"/> <include file="changelogs/db.changelog-2.6.xml" />
<include file="changelogs/db.changelog-3.2.xml"/> <include file="changelogs/db.changelog-3.2.xml" />
<include file="changelogs/db.changelog-3.5.xml" />
</databaseChangeLog> </databaseChangeLog>

View File

@@ -1,7 +1,6 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import org.apache.http.HttpHeaders; import org.apache.http.HttpHeaders;
import org.hibernate.SessionFactory;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@@ -16,6 +15,7 @@ import org.mockserver.model.HttpResponse;
import org.mockserver.model.MediaType; import org.mockserver.model.MediaType;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
@ExtendWith(MockServerExtension.class) @ExtendWith(MockServerExtension.class)
@@ -28,7 +28,7 @@ class PubSubServiceTest {
private FeedService feedService; private FeedService feedService;
@Mock(answer = Answers.RETURNS_DEEP_STUBS) @Mock(answer = Answers.RETURNS_DEEP_STUBS)
private SessionFactory sessionFactory; private UnitOfWork unitOfWork;
@Mock @Mock
private Feed feed; private Feed feed;
@@ -43,7 +43,7 @@ class PubSubServiceTest {
this.client = client; this.client = client;
this.client.reset(); this.client.reset();
this.underTest = new PubSubService(config, feedService, sessionFactory); this.underTest = new PubSubService(config, feedService, unitOfWork);
Integer port = client.getPort(); Integer port = client.getPort();
String hubUrl = String.format("http://localhost:%s/hub", port); String hubUrl = String.format("http://localhost:%s/hub", port);
@@ -72,7 +72,7 @@ class PubSubServiceTest {
.withMethod("POST") .withMethod("POST")
.withPath("/hub")); .withPath("/hub"));
Mockito.verify(feed, Mockito.never()).setPushTopic(Mockito.anyString()); Mockito.verify(feed, Mockito.never()).setPushTopic(Mockito.anyString());
Mockito.verifyNoInteractions(feedService); Mockito.verifyNoInteractions(unitOfWork);
} }
@Test @Test
@@ -86,7 +86,7 @@ class PubSubServiceTest {
// Assert // Assert
Mockito.verify(feed).setPushTopic(Mockito.anyString()); Mockito.verify(feed).setPushTopic(Mockito.anyString());
Mockito.verify(feedService).save(feed); Mockito.verify(unitOfWork).run(Mockito.any());
} }
@Test @Test
@@ -99,7 +99,7 @@ class PubSubServiceTest {
// Assert // Assert
Mockito.verify(feed, Mockito.never()).setPushTopic(Mockito.anyString()); Mockito.verify(feed, Mockito.never()).setPushTopic(Mockito.anyString());
Mockito.verifyNoInteractions(feedService); Mockito.verifyNoInteractions(unitOfWork);
} }
} }

View File

@@ -1,6 +1,6 @@
package com.commafeed.frontend.resource; package com.commafeed.frontend.resource;
import java.util.Arrays; import java.util.Collections;
import java.util.Optional; import java.util.Optional;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -76,7 +76,7 @@ class UserRestTest {
userREST.registerUser(req, sessionHelper); userREST.registerUser(req, sessionHelper);
inOrder.verify(service).register("user", "password", "test@test.com", Arrays.asList(Role.USER)); inOrder.verify(service).register("user", "password", "test@test.com", Collections.singletonList(Role.USER));
inOrder.verify(service).login("user", "password"); inOrder.verify(service).login("user", "password");
} }

View File

@@ -4,8 +4,11 @@ app:
# url used to access commafeed # url used to access commafeed
publicUrl: http://localhost:8082/ publicUrl: http://localhost:8082/
# wether to allow user registrations # whether to allow user registrations
allowRegistrations: true allowRegistrations: true
# whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char)
strictPasswordPolicy: true
# create a demo account the first time the app starts # create a demo account the first time the app starts
createDemoAccount: false createDemoAccount: false
@@ -37,14 +40,14 @@ app:
graphitePort: 2003 graphitePort: 2003
graphiteInterval: 60 graphiteInterval: 60
# wether this commafeed instance has a lot of feeds to refresh # whether this commafeed instance has a lot of feeds to refresh
# leave this to false in almost all cases # leave this to false in almost all cases
heavyLoad: false heavyLoad: false
# minimum amount of time commafeed will wait before refreshing the same feed # minimum amount of time commafeed will wait before refreshing the same feed
refreshIntervalMinutes: 5 refreshIntervalMinutes: 5
# wether to enable pubsub # whether to enable pubsub
# probably not needed if refreshIntervalMinutes is low # probably not needed if refreshIntervalMinutes is low
pubsubhubbub: false pubsubhubbub: false
@@ -60,6 +63,9 @@ app:
# entries to keep per feed, old entries will be deleted, 0 to disable # entries to keep per feed, old entries will be deleted, 0 to disable
maxFeedCapacity: 500 maxFeedCapacity: 500
# limit the number of feeds a user can subscribe to, 0 to disable
maxFeedsPerUser: 0
# cache service to use, possible values are 'noop' and 'redis' # cache service to use, possible values are 'noop' and 'redis'
cache: noop cache: noop
@@ -69,7 +75,7 @@ app:
# user-agent string that will be used by the http client, leave empty for the default one # user-agent string that will be used by the http client, leave empty for the default one
userAgent: userAgent:
# Database connection # Database connection
# ------------------- # -------------------
# for MySQL # for MySQL
@@ -92,8 +98,8 @@ database:
properties: properties:
charSet: UTF-8 charSet: UTF-8
validationQuery: "/* CommaFeed Health Check */ SELECT 1" validationQuery: "/* CommaFeed Health Check */ SELECT 1"
logging: logging:
level: INFO level: INFO
loggers: loggers:

View File

@@ -5,7 +5,7 @@
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>3.4.0</version> <version>3.5.0</version>
<name>CommaFeed</name> <name>CommaFeed</name>
<packaging>pom</packaging> <packaging>pom</packaging>