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
## [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]
- 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
- OPML import/export
- REST API
## 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)
- [Browser extension](https://github.com/Athou/commafeed-browser-extension)
## Deployment on your own server

View File

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

View File

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

View File

@@ -13,6 +13,8 @@ export const redirectToRegistration = createAsyncThunk("redirect/register", (_,
export const redirectToPasswordRecovery = createAsyncThunk("redirect/passwordRecovery", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/passwordRecovery"))
)
export const redirectToApiDocumentation = createAsyncThunk("redirect/api", (_, thunkApi) => thunkApi.dispatch(redirectTo("/api")))
export const redirectToSelectedSource = createAsyncThunk<
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 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({
name: "redirect",

View File

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

View File

@@ -16,6 +16,7 @@ import { FeedEntryHeader } from "./FeedEntryHeader"
interface FeedEntryProps {
entry: Entry
expanded: boolean
selected: boolean
showSelectionIndicator: boolean
onHeaderClick: (e: React.MouseEvent) => void
}
@@ -72,7 +73,7 @@ const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: View
export function FeedEntry(props: FeedEntryProps) {
const { viewMode } = useViewMode()
const { classes } = useStyles({ ...props, viewMode })
const { classes, cx } = useStyles({ ...props, viewMode })
const dispatch = useAppDispatch()
@@ -95,7 +96,17 @@ export function FeedEntry(props: FeedEntryProps) {
const compactHeader = !props.expanded && (viewMode === "title" || viewMode === "cozy")
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
className={classes.headerLink}
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 { App } from "App"
import { store } from "app/store"
import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
import "main.css"
import "react-contexify/ReactContexify.css"
import ReactDOM from "react-dom/client"
import { Provider } from "react-redux"
import { App } from "./App"
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 { Loader } from "components/Loader"
import { Gauge } from "components/metrics/Gauge"
import { Meter } from "components/metrics/Meter"
import { MetricAccordionItem } from "components/metrics/MetricAccordionItem"
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",
}
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() {
const query = useAsync(() => client.admin.getMetrics(), [])
if (!query.result) return <Loader />
const { meters, timers } = query.result.data
const { meters, gauges, timers } = query.result.data
return (
<Tabs defaultValue="stats">
<Tabs.List>
@@ -39,6 +46,15 @@ export function MetricsPage() {
</MetricAccordionItem>
))}
</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 value="timers" pt="xs">

View File

@@ -86,28 +86,9 @@ export function AboutPage() {
<Section title={<Trans>Goodies</Trans>} icon={<TbPuzzle size={24} />}>
<List>
<List.Item>
<Trans>Browser extentions</Trans>
<List withPadding>
<List.Item>
<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>
<Anchor href="https://github.com/Athou/commafeed-browser-extension" target="_blank" rel="noreferrer">
<Trans>Browser extentions</Trans>
</Anchor>
</List.Item>
<List.Item>
<Trans>Subscribe URL</Trans>

View File

@@ -4,8 +4,11 @@ app:
# url used to access commafeed
publicUrl: http://localhost:8082/
# wether to allow user registrations
# whether to allow user registrations
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
createDemoAccount: true
@@ -37,14 +40,14 @@ app:
graphitePort: 2003
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
heavyLoad: false
# minimum amount of time commafeed will wait before refreshing the same feed
refreshIntervalMinutes: 5
# wether to enable pubsub
# whether to enable pubsub
# probably not needed if refreshIntervalMinutes is low
pubsubhubbub: false
@@ -60,7 +63,10 @@ app:
# entries to keep per feed, old entries will be deleted, 0 to disable
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: noop

View File

@@ -6,6 +6,9 @@ app:
# whether to allow user registrations
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
createDemoAccount: false
@@ -38,14 +41,14 @@ app:
graphitePort: 2003
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
heavyLoad: false
# minimum amount of time commafeed will wait before refreshing the same feed
refreshIntervalMinutes: 5
# wether to enable pubsub
# whether to enable pubsub
# probably not needed if refreshIntervalMinutes is low
pubsubhubbub: false
@@ -61,7 +64,10 @@ app:
# entries to keep per feed, old entries will be deleted, 0 to disable
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: noop

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>3.4.0</version>
<version>3.5.0</version>
</parent>
<artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name>
@@ -226,7 +226,7 @@
<dependency>
<groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId>
<version>3.4.0</version>
<version>3.5.0</version>
</dependency>
<dependency>
@@ -278,11 +278,6 @@
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-json</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard.modules</groupId>
<artifactId>dropwizard-web</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>be.tomcools</groupId>
<artifactId>dropwizard-websocket-jee7-bundle</artifactId>
@@ -374,7 +369,7 @@
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.2</version>
<version>4.4.1</version>
</dependency>
<dependency>
<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.UserService;
import com.commafeed.backend.task.ScheduledTask;
import com.commafeed.frontend.auth.PasswordConstraintValidator;
import com.commafeed.frontend.auth.SecurityCheckFactoryProvider;
import com.commafeed.frontend.resource.AdminREST;
import com.commafeed.frontend.resource.CategoryREST;
@@ -69,8 +70,6 @@ import io.dropwizard.migrations.MigrationsBundle;
import io.dropwizard.servlets.CacheBustingFilter;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import io.dropwizard.web.WebBundle;
import io.dropwizard.web.conf.WebConfiguration;
import io.whitfin.dropwizard.configuration.EnvironmentSubstitutor;
public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
@@ -118,24 +117,16 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
DataSourceFactory factory = configuration.getDataSourceFactory();
// keep using old id generator for backward compatibility
factory.getProperties().put(AvailableSettings.USE_NEW_ID_GENERATOR_MAPPINGS, "false");
factory.getProperties().put(AvailableSettings.PREFERRED_POOLED_OPTIMIZER, "pooled-lo");
factory.getProperties().put(AvailableSettings.STATEMENT_BATCH_SIZE, "50");
factory.getProperties().put(AvailableSettings.BATCH_VERSIONED_DATA, "true");
factory.getProperties().put(AvailableSettings.ORDER_INSERTS, "true");
factory.getProperties().put(AvailableSettings.ORDER_UPDATES, "true");
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>() {
@Override
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
@@ -149,6 +140,8 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
@Override
public void run(CommaFeedConfiguration config, Environment environment) throws Exception {
PasswordConstraintValidator.setStrict(config.getApplicationSettings().getStrictPasswordPolicy());
// guice init
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.db.DataSourceFactory;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CommaFeedConfiguration extends Configuration {
public enum CacheType {
@@ -56,6 +58,7 @@ public class CommaFeedConfiguration extends Configuration {
}
@Getter
@Setter
public static class ApplicationSettings {
@NotNull
@NotBlank
@@ -66,6 +69,10 @@ public class CommaFeedConfiguration extends Configuration {
@Valid
private Boolean allowRegistrations;
@NotNull
@Valid
private Boolean strictPasswordPolicy = true;
@NotNull
@Valid
private Boolean createDemoAccount;
@@ -124,6 +131,10 @@ public class CommaFeedConfiguration extends Configuration {
@Valid
private Integer maxFeedCapacity;
@NotNull
@Valid
private Integer maxFeedsPerUser = 0;
@NotNull
@Min(0)
@Valid

View File

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

View File

@@ -18,7 +18,7 @@ import com.querydsl.core.types.Predicate;
@Singleton
public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
private QFeedCategory category = QFeedCategory.feedCategory;
private final QFeedCategory category = QFeedCategory.feedCategory;
@Inject
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.QFeed;
import com.commafeed.backend.model.QFeedSubscription;
import com.commafeed.backend.model.QUser;
import com.google.common.collect.Iterables;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.JPQLQuery;
@Singleton
public class FeedDAO extends GenericDAO<Feed> {
@@ -28,18 +26,12 @@ public class FeedDAO extends GenericDAO<Feed> {
super(sessionFactory);
}
public List<Feed> findNextUpdatable(int count, Date lastLoginThreshold) {
JPQLQuery<Feed> query = query().selectFrom(feed);
query.where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(new Date())));
if (lastLoginThreshold != null) {
QFeedSubscription subs = QFeedSubscription.feedSubscription;
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 List<Feed> findNextUpdatable(int count) {
return query().selectFrom(feed)
.where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(new Date())))
.orderBy(feed.disabledUntil.asc())
.limit(count)
.fetch();
}
public void setDisabledUntil(List<Long> feedIds, Date date) {

View File

@@ -21,7 +21,7 @@ import lombok.Getter;
@Singleton
public class FeedEntryDAO extends GenericDAO<FeedEntry> {
private QFeedEntry entry = QFeedEntry.feedEntry;
private final QFeedEntry entry = QFeedEntry.feedEntry;
@Inject
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) {
if (status == null) {
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.setRead(read);
status.setMarkable(!read);

View File

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

View File

@@ -21,7 +21,7 @@ import com.querydsl.jpa.JPQLQuery;
@Singleton
public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
private QFeedSubscription sub = QFeedSubscription.feedSubscription;
private final QFeedSubscription sub = QFeedSubscription.feedSubscription;
@Inject
public FeedSubscriptionDAO(SessionFactory sessionFactory) {
@@ -59,6 +59,10 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
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) {
JPQLQuery<FeedSubscription> query = query().selectFrom(sub).where(sub.user.eq(user));
if (category == null) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -73,7 +73,7 @@ public class FeedFetcher {
boolean etagHeaderValueChanged = !StringUtils.equals(eTag, result.getETag());
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);
throw new NotModifiedException("content hash not modified",
lastModifiedHeaderValueChanged ? result.getLastModifiedSince() : null,

View File

@@ -16,8 +16,8 @@ import javax.inject.Inject;
import javax.inject.Singleton;
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.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
@@ -33,7 +33,7 @@ import lombok.extern.slf4j.Slf4j;
@Singleton
public class FeedRefreshEngine implements Managed {
private final SessionFactory sessionFactory;
private final UnitOfWork unitOfWork;
private final FeedDAO feedDAO;
private final FeedRefreshWorker worker;
private final FeedRefreshUpdater updater;
@@ -45,13 +45,13 @@ public class FeedRefreshEngine implements Managed {
private final ExecutorService feedProcessingLoopExecutor;
private final ExecutorService refillLoopExecutor;
private final ExecutorService refillExecutor;
private final ExecutorService workerExecutor;
private final ExecutorService databaseUpdaterExecutor;
private final ThreadPoolExecutor workerExecutor;
private final ThreadPoolExecutor databaseUpdaterExecutor;
@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) {
this.sessionFactory = sessionFactory;
this.unitOfWork = unitOfWork;
this.feedDAO = feedDAO;
this.worker = worker;
this.updater = updater;
@@ -65,6 +65,10 @@ public class FeedRefreshEngine implements Managed {
this.refillExecutor = newDiscardingSingleThreadExecutorService();
this.workerExecutor = newBlockingExecutorService(config.getApplicationSettings().getBackgroundThreads());
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
@@ -156,8 +160,8 @@ public class FeedRefreshEngine implements Managed {
}
private List<Feed> getNextUpdatableFeeds(int max) {
return UnitOfWork.call(sessionFactory, () -> {
List<Feed> feeds = feedDAO.findNextUpdatable(max, getLastLoginThreshold());
return unitOfWork.call(() -> {
List<Feed> feeds = feedDAO.findNextUpdatable(max);
// update disabledUntil to prevent feeds from being returned again by feedDAO.findNextUpdatable()
Date nextUpdateDate = DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes());
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());
}
private Date getLastLoginThreshold() {
return Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad()) ? DateUtils.addDays(new Date(), -30) : null;
}
@Override
public void stop() {
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
*/
private ExecutorService newDiscardingSingleThreadExecutorService() {
private ThreadPoolExecutor newDiscardingSingleThreadExecutorService() {
ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
return pool;
@@ -194,7 +194,7 @@ public class FeedRefreshEngine implements Managed {
/**
* 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<>());
pool.setRejectedExecutionHandler((r, e) -> {
if (e.isShutdown()) {

View File

@@ -13,8 +13,8 @@ import com.commafeed.backend.model.Feed;
@Singleton
public class FeedRefreshIntervalCalculator {
private boolean heavyLoad;
private int refreshIntervalMinutes;
private final boolean heavyLoad;
private final int refreshIntervalMinutes;
@Inject
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.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.SessionFactory;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
@@ -46,7 +45,7 @@ import lombok.extern.slf4j.Slf4j;
@Singleton
public class FeedRefreshUpdater implements Managed {
private final SessionFactory sessionFactory;
private final UnitOfWork unitOfWork;
private final FeedService feedService;
private final FeedEntryService feedEntryService;
private final PubSubService pubSubService;
@@ -63,10 +62,10 @@ public class FeedRefreshUpdater implements Managed {
private final Meter entryInserted;
@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,
CacheService cache, WebSocketSessions webSocketSessions) {
this.sessionFactory = sessionFactory;
this.unitOfWork = unitOfWork;
this.feedService = feedService;
this.feedEntryService = feedEntryService;
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
// 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
// twice at the same time
@@ -107,7 +106,7 @@ public class FeedRefreshUpdater implements Managed {
locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
if (locked1 && locked2) {
processed = true;
inserted = UnitOfWork.call(sessionFactory, () -> feedEntryService.addEntry(feed, entry, subscriptions));
inserted = unitOfWork.call(() -> feedEntryService.addEntry(feed, entry, subscriptions));
if (inserted) {
entryInserted.mark();
}
@@ -164,7 +163,7 @@ public class FeedRefreshUpdater implements Managed {
if (!lastEntries.contains(cacheKey)) {
log.debug("cache miss for {}", entry.getUrl());
if (subscriptions == null) {
subscriptions = UnitOfWork.call(sessionFactory, () -> feedSubscriptionDAO.findByFeed(feed));
subscriptions = unitOfWork.call(() -> feedSubscriptionDAO.findByFeed(feed));
}
AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
processed &= addEntryResult.processed;
@@ -204,7 +203,7 @@ public class FeedRefreshUpdater implements Managed {
feedUpdated.mark();
}
UnitOfWork.run(sessionFactory, () -> feedService.save(feed));
unitOfWork.run(() -> feedService.save(feed));
return processed;
}

View File

@@ -154,7 +154,7 @@ public class FeedUtils {
for (Emit emit : emits) {
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()));
prevIndex = emit.getEnd() + 1;
}
@@ -228,7 +228,7 @@ public class FeedUtils {
if (index == -1) {
return null;
}
String encoding = pi.substring(index + 10, pi.length());
String encoding = pi.substring(index + 10);
encoding = encoding.substring(0, encoding.indexOf('"'));
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.User;
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.Outline;
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
try {
feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent, position);
} catch (FeedSubscriptionException e) {
throw e;
} catch (Exception e) {
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) {
Element e = document.getRootElement();
if (e.getName().equals("opml")) {
return true;
}
return false;
return e.getName().equals("opml");
}

View File

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

View File

@@ -27,6 +27,7 @@ import lombok.extern.slf4j.Slf4j;
@Singleton
public class DatabaseStartupService implements Managed {
private final UnitOfWork unitOfWork;
private final SessionFactory sessionFactory;
private final UserDAO userDAO;
private final UserService userService;
@@ -35,9 +36,9 @@ public class DatabaseStartupService implements Managed {
@Override
public void start() {
updateSchema();
long count = UnitOfWork.call(sessionFactory, userDAO::count);
long count = unitOfWork.call(userDAO::count);
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 Set<AbstractFaviconFetcher> faviconFetchers;
private Favicon defaultFavicon;
private final Favicon defaultFavicon;
@Inject
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");
}
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);
// upgrade feed to https if it was using http

View File

@@ -41,9 +41,9 @@ public class MailService {
Properties props = new Properties();
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.port", "" + settings.getSmtpPort());
props.put("mail.smtp.port", String.valueOf(settings.getSmtpPort()));
Session session = Session.getInstance(props, new Authenticator() {
@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.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.hibernate.SessionFactory;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpGetter;
@@ -40,7 +39,7 @@ public class PubSubService {
private final CommaFeedConfiguration config;
private final FeedService feedService;
private final SessionFactory sessionFactory;
private final UnitOfWork unitOfWork;
public void subscribe(Feed feed) {
String hub = feed.getPushHub();
@@ -75,7 +74,7 @@ public class PubSubService {
if (code == 400 && StringUtils.contains(message, pushpressError)) {
String[] tokens = message.split(" ");
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());
} else {
throw new Exception(

View File

@@ -2,6 +2,7 @@ package com.commafeed.backend.service;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Optional;
import java.util.Set;
@@ -123,7 +124,7 @@ public class UserService {
}
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) {

View File

@@ -8,6 +8,7 @@ import javax.inject.Singleton;
import org.apache.commons.lang3.time.DateUtils;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.model.User;
import com.commafeed.backend.service.FeedSubscriptionService;
@@ -20,6 +21,7 @@ public class PostLoginActivities {
private final UserDAO userDAO;
private final FeedSubscriptionService feedSubscriptionService;
private final UnitOfWork unitOfWork;
private final CommaFeedConfiguration config;
public void executeFor(User user) {
@@ -27,19 +29,26 @@ public class PostLoginActivities {
Date now = new Date();
boolean saveUser = false;
// only update lastLogin field every hour in order to not
// invalidate the cache every time someone logs in
if (lastLogin == null || lastLogin.before(DateUtils.addHours(now, -1))) {
user.setLastLogin(now);
saveUser = true;
}
if (config.getApplicationSettings().getHeavyLoad() && user.shouldRefreshFeedsAt(now)) {
if (Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad()) && user.shouldRefreshFeedsAt(now)) {
feedSubscriptionService.refreshAll(user);
user.setLastFullRefresh(now);
saveUser = true;
}
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.Singleton;
import org.hibernate.SessionFactory;
import com.commafeed.CommaFeedApplication;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.UnitOfWork;
@@ -23,7 +21,7 @@ import lombok.extern.slf4j.Slf4j;
public class DemoAccountCleanupTask extends ScheduledTask {
private final CommaFeedConfiguration config;
private final SessionFactory sessionFactory;
private final UnitOfWork unitOfWork;
private final UserDAO userDAO;
private final UserService userService;
@@ -34,7 +32,7 @@ public class DemoAccountCleanupTask extends ScheduledTask {
}
log.info("recreating demo user account");
UnitOfWork.run(sessionFactory, () -> {
unitOfWork.run(() -> {
User demoUser = userDAO.findByName(CommaFeedApplication.USERNAME_DEMO);
if (demoUser == null) {
return;

View File

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

View File

@@ -31,7 +31,7 @@ public class Category implements Serializable {
@ApiModelProperty(value = "category feeds", required = true)
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;
@ApiModelProperty(value = "position of the category in the list", required = true)

View File

@@ -1,7 +1,7 @@
package com.commafeed.frontend.model;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@@ -45,7 +45,7 @@ public class Entry implements Serializable {
@ApiModelProperty(value = "comma-separated list of 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;
@ApiModelProperty(value = "entry author")
@@ -99,7 +99,7 @@ public class Entry implements Serializable {
@ApiModelProperty(value = "starred status", required = true)
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;
@ApiModelProperty(value = "tags", required = true)
@@ -158,13 +158,13 @@ public class Entry implements Serializable {
SyndContentImpl content = new SyndContentImpl();
content.setValue(getContent());
entry.setContents(Arrays.<SyndContent> asList(content));
entry.setContents(Collections.<SyndContent> singletonList(content));
if (getEnclosureUrl() != null) {
SyndEnclosureImpl enclosure = new SyndEnclosureImpl();
enclosure.setType(getEnclosureType());
enclosure.setUrl(getEnclosureUrl());
entry.setEnclosures(Arrays.<SyndEnclosure> asList(enclosure));
entry.setEnclosures(Collections.<SyndEnclosure> singletonList(enclosure));
}
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 = "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 = "ordering",
allowableValues = "asc,desc,abc,zyx") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
@ApiParam(value = "ordering", allowableValues = "asc,desc") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
@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,
@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.StringWriter;
import java.net.URI;
import java.util.Arrays;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
@@ -141,9 +141,7 @@ public class FeedREST {
@ApiParam(value = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
@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 = "ordering",
allowableValues = "asc,desc,abc,zyx") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
@ApiParam(value = "ordering", allowableValues = "asc,desc") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
@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,
@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.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);
for (FeedEntryStatus status : list) {
@@ -325,7 +323,7 @@ public class FeedREST {
FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(req.getId()));
if (subscription != null) {
feedEntryService.markSubscriptionEntries(user, Arrays.asList(subscription), olderThan, entryKeywords);
feedEntryService.markSubscriptionEntries(user, Collections.singletonList(subscription), olderThan, entryKeywords);
}
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();
}
try {
String opml = IOUtils.toString(input, "UTF-8");
String opml = IOUtils.toString(input, StandardCharsets.UTF_8);
opmlImporter.importOpml(user, opml);
} catch (Exception e) {
log.error(e.getMessage(), e);

View File

@@ -1,6 +1,6 @@
package com.commafeed.frontend.resource;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Optional;
import java.util.UUID;
@@ -227,7 +227,8 @@ public class UserREST {
public Response registerUser(@Valid @ApiParam(required = true) RegistrationRequest req,
@Context @ApiParam(hidden = true) SessionHelper sessionHelper) {
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());
sessionHelper.setLoggedInUser(registeredUser);
return Response.ok().build();

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.SessionFactory;
import com.commafeed.CommaFeedConfiguration;
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_READINGORDER = "order";
private final SessionFactory sessionFactory;
private final UnitOfWork unitOfWork;
private final FeedSubscriptionDAO feedSubscriptionDAO;
private final FeedEntryStatusDAO feedEntryStatusDAO;
private final FeedCategoryDAO feedCategoryDAO;
@@ -54,7 +53,7 @@ public class NextUnreadServlet extends HttpServlet {
SessionHelper sessionHelper = new SessionHelper(req);
Optional<User> user = sessionHelper.getLoggedInUser();
if (user.isPresent()) {
UnitOfWork.run(sessionFactory, () -> userService.performPostLoginActivities(user.get()));
unitOfWork.run(() -> userService.performPostLoginActivities(user.get()));
}
if (!user.isPresent()) {
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;
FeedEntryStatus status = UnitOfWork.call(sessionFactory, () -> {
FeedEntryStatus status = unitOfWork.call(() -> {
FeedEntryStatus s = null;
if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user.get());

View File

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

View File

@@ -7,7 +7,7 @@ import lombok.experimental.UtilityClass;
@UtilityClass
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());
}

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"
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="blob" dbms="h2"/>
<property name="blob_type" value="blob" dbms="mysql,mariadb"/>
<property name="blob_type" value="blob" dbms="mssql"/>
<property name="blob_type" value="bytea" dbms="postgresql" />
<property name="blob_type" value="blob" dbms="h2" />
<property name="blob_type" value="blob" dbms="mysql,mariadb" />
<property name="blob_type" value="blob" dbms="mssql" />
<include file="changelogs/db.changelog-1.0.xml"/>
<include file="changelogs/db.changelog-1.1.xml"/>
<include file="changelogs/db.changelog-1.2.xml"/>
<include file="changelogs/db.changelog-1.3.xml"/>
<include file="changelogs/db.changelog-1.4.xml"/>
<include file="changelogs/db.changelog-1.5.xml"/>
<include file="changelogs/db.changelog-2.1.xml"/>
<include file="changelogs/db.changelog-2.2.xml"/>
<include file="changelogs/db.changelog-2.6.xml"/>
<include file="changelogs/db.changelog-3.2.xml"/>
<include file="changelogs/db.changelog-1.0.xml" />
<include file="changelogs/db.changelog-1.1.xml" />
<include file="changelogs/db.changelog-1.2.xml" />
<include file="changelogs/db.changelog-1.3.xml" />
<include file="changelogs/db.changelog-1.4.xml" />
<include file="changelogs/db.changelog-1.5.xml" />
<include file="changelogs/db.changelog-2.1.xml" />
<include file="changelogs/db.changelog-2.2.xml" />
<include file="changelogs/db.changelog-2.6.xml" />
<include file="changelogs/db.changelog-3.2.xml" />
<include file="changelogs/db.changelog-3.5.xml" />
</databaseChangeLog>

View File

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

View File

@@ -4,8 +4,11 @@ app:
# url used to access commafeed
publicUrl: http://localhost:8082/
# wether to allow user registrations
# whether to allow user registrations
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
createDemoAccount: false
@@ -37,14 +40,14 @@ app:
graphitePort: 2003
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
heavyLoad: false
# minimum amount of time commafeed will wait before refreshing the same feed
refreshIntervalMinutes: 5
# wether to enable pubsub
# whether to enable pubsub
# probably not needed if refreshIntervalMinutes is low
pubsubhubbub: false
@@ -60,6 +63,9 @@ app:
# entries to keep per feed, old entries will be deleted, 0 to disable
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: noop
@@ -69,7 +75,7 @@ app:
# user-agent string that will be used by the http client, leave empty for the default one
userAgent:
# Database connection
# -------------------
# for MySQL
@@ -92,8 +98,8 @@ database:
properties:
charSet: UTF-8
validationQuery: "/* CommaFeed Health Check */ SELECT 1"
logging:
level: INFO
loggers:

View File

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