mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
removed wicket and tomee, use dropwizard instead. remove wro4j, use gulp instead
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
public class ApplicationPropertiesService {
|
||||
|
||||
private ResourceBundle bundle;
|
||||
|
||||
public ApplicationPropertiesService() {
|
||||
bundle = ResourceBundle.getBundle("application");
|
||||
}
|
||||
|
||||
public String getDatasource() {
|
||||
return bundle.getString("datasource");
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return bundle.getString("version");
|
||||
}
|
||||
|
||||
public String getGitCommit() {
|
||||
return bundle.getString("git.commit");
|
||||
}
|
||||
|
||||
public boolean isProduction() {
|
||||
return Boolean.valueOf(bundle.getString("production"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import com.commafeed.backend.dao.FeedDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryContentDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryStatus;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
|
||||
/**
|
||||
* Contains utility methods for cleaning the database
|
||||
*
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class DatabaseCleaningService {
|
||||
|
||||
private static final int BATCH_SIZE = 100;
|
||||
|
||||
private final FeedDAO feedDAO;
|
||||
private final FeedEntryDAO feedEntryDAO;
|
||||
private final FeedEntryContentDAO feedEntryContentDAO;
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
|
||||
public long cleanEntriesWithoutSubscriptions() {
|
||||
log.info("cleaning entries without subscriptions");
|
||||
long total = 0;
|
||||
int deleted = 0;
|
||||
do {
|
||||
List<FeedEntry> entries = feedEntryDAO.findWithoutSubscriptions(BATCH_SIZE);
|
||||
deleted = feedEntryDAO.delete(entries);
|
||||
total += deleted;
|
||||
log.info("removed {} entries without subscriptions", total);
|
||||
} while (deleted != 0);
|
||||
log.info("cleanup done: {} entries without subscriptions deleted", total);
|
||||
return total;
|
||||
}
|
||||
|
||||
public long cleanFeedsWithoutSubscriptions() {
|
||||
log.info("cleaning feeds without subscriptions");
|
||||
long total = 0;
|
||||
int deleted = 0;
|
||||
do {
|
||||
List<Feed> feeds = feedDAO.findWithoutSubscriptions(BATCH_SIZE);
|
||||
deleted = feedDAO.delete(feeds);
|
||||
total += deleted;
|
||||
log.info("removed {} feeds without subscriptions", total);
|
||||
} while (deleted != 0);
|
||||
log.info("cleanup done: {} feeds without subscriptions deleted", total);
|
||||
return total;
|
||||
}
|
||||
|
||||
public long cleanContentsWithoutEntries() {
|
||||
log.info("cleaning contents without entries");
|
||||
long total = 0;
|
||||
int deleted = 0;
|
||||
do {
|
||||
deleted = feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE);
|
||||
total += deleted;
|
||||
log.info("removed {} contents without entries", total);
|
||||
} while (deleted != 0);
|
||||
log.info("cleanup done: {} contents without entries deleted", total);
|
||||
return total;
|
||||
}
|
||||
|
||||
public long cleanEntriesOlderThan(long value, TimeUnit unit) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.add(Calendar.MINUTE, -1 * (int) unit.toMinutes(value));
|
||||
|
||||
long total = 0;
|
||||
int deleted = 0;
|
||||
do {
|
||||
deleted = feedEntryDAO.delete(cal.getTime(), BATCH_SIZE);
|
||||
total += deleted;
|
||||
log.info("removed {} entries", total);
|
||||
} while (deleted != 0);
|
||||
log.info("cleanup done: {} entries deleted", total);
|
||||
return total;
|
||||
}
|
||||
|
||||
public void mergeFeeds(Feed into, List<Feed> feeds) {
|
||||
for (Feed feed : feeds) {
|
||||
if (into.getId().equals(feed.getId())) {
|
||||
continue;
|
||||
}
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findByFeed(feed);
|
||||
for (FeedSubscription sub : subs) {
|
||||
sub.setFeed(into);
|
||||
}
|
||||
feedSubscriptionDAO.saveOrUpdate(subs);
|
||||
feedDAO.delete(feed);
|
||||
}
|
||||
feedDAO.saveOrUpdate(into);
|
||||
}
|
||||
|
||||
public long cleanStatusesOlderThan(Date olderThan) {
|
||||
log.info("cleaning old read statuses");
|
||||
long total = 0;
|
||||
List<FeedEntryStatus> list = Collections.emptyList();
|
||||
do {
|
||||
list = feedEntryStatusDAO.getOldStatuses(olderThan, BATCH_SIZE);
|
||||
if (!list.isEmpty()) {
|
||||
feedEntryStatusDAO.delete(list);
|
||||
total += list.size();
|
||||
log.info("cleaned {} old read statuses", total);
|
||||
}
|
||||
} while (!list.isEmpty());
|
||||
log.info("cleanup done: {} old read statuses deleted", total);
|
||||
return total;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import com.commafeed.backend.dao.FeedEntryContentDAO;
|
||||
import com.commafeed.backend.feed.FeedUtils;
|
||||
import com.commafeed.backend.model.FeedEntryContent;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class FeedEntryContentService {
|
||||
|
||||
private final FeedEntryContentDAO feedEntryContentDAO;
|
||||
|
||||
/**
|
||||
* this is NOT thread-safe
|
||||
*/
|
||||
public FeedEntryContent findOrCreate(FeedEntryContent content, String baseUrl) {
|
||||
|
||||
String contentHash = DigestUtils.sha1Hex(StringUtils.trimToEmpty(content.getContent()));
|
||||
String titleHash = DigestUtils.sha1Hex(StringUtils.trimToEmpty(content.getTitle()));
|
||||
Long existingId = feedEntryContentDAO.findExisting(contentHash, titleHash);
|
||||
|
||||
FeedEntryContent result = null;
|
||||
if (existingId == null) {
|
||||
content.setContentHash(contentHash);
|
||||
content.setTitleHash(titleHash);
|
||||
|
||||
content.setAuthor(FeedUtils.truncate(FeedUtils.handleContent(content.getAuthor(), baseUrl, true), 128));
|
||||
content.setTitle(FeedUtils.truncate(FeedUtils.handleContent(content.getTitle(), baseUrl, true), 2048));
|
||||
content.setContent(FeedUtils.handleContent(content.getContent(), baseUrl, false));
|
||||
result = content;
|
||||
feedEntryContentDAO.saveOrUpdate(result);
|
||||
} else {
|
||||
result = new FeedEntryContent();
|
||||
result.setId(existingId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryStatus;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class FeedEntryService {
|
||||
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
private final FeedEntryDAO feedEntryDAO;
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
private final CacheService cache;
|
||||
|
||||
public void markEntry(User user, Long entryId, boolean read) {
|
||||
|
||||
FeedEntry entry = feedEntryDAO.findById(entryId);
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, entry.getFeed());
|
||||
if (sub == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry);
|
||||
if (status.isMarkable()) {
|
||||
status.setRead(read);
|
||||
feedEntryStatusDAO.saveOrUpdate(status);
|
||||
cache.invalidateUnreadCount(sub);
|
||||
cache.invalidateUserRootCategory(user);
|
||||
}
|
||||
}
|
||||
|
||||
public void starEntry(User user, Long entryId, Long subscriptionId, boolean starred) {
|
||||
|
||||
FeedSubscription sub = feedSubscriptionDAO.findById(user, subscriptionId);
|
||||
if (sub == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
FeedEntry entry = feedEntryDAO.findById(entryId);
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry);
|
||||
status.setStarred(starred);
|
||||
feedEntryStatusDAO.saveOrUpdate(status);
|
||||
}
|
||||
|
||||
public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Date olderThan) {
|
||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, -1, -1, null, false,
|
||||
false, null);
|
||||
markList(statuses, olderThan);
|
||||
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
|
||||
cache.invalidateUserRootCategory(user);
|
||||
}
|
||||
|
||||
public void markStarredEntries(User user, Date olderThan) {
|
||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findStarred(user, null, -1, -1, null, false);
|
||||
markList(statuses, olderThan);
|
||||
}
|
||||
|
||||
private void markList(List<FeedEntryStatus> statuses, Date olderThan) {
|
||||
List<FeedEntryStatus> list = Lists.newArrayList();
|
||||
for (FeedEntryStatus status : statuses) {
|
||||
if (!status.isRead()) {
|
||||
Date inserted = status.getEntry().getInserted();
|
||||
if (olderThan == null || inserted == null || olderThan.after(inserted)) {
|
||||
status.setRead(true);
|
||||
list.add(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
feedEntryStatusDAO.saveOrUpdate(list);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryTagDAO;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryTag;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class FeedEntryTagService {
|
||||
|
||||
private final FeedEntryDAO feedEntryDAO;
|
||||
private final FeedEntryTagDAO feedEntryTagDAO;
|
||||
|
||||
public void updateTags(User user, Long entryId, List<String> tagNames) {
|
||||
FeedEntry entry = feedEntryDAO.findById(entryId);
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<FeedEntryTag> tags = feedEntryTagDAO.findByEntry(user, entry);
|
||||
Map<String, FeedEntryTag> tagMap = Maps.uniqueIndex(tags, new Function<FeedEntryTag, String>() {
|
||||
@Override
|
||||
public String apply(FeedEntryTag input) {
|
||||
return input.getName();
|
||||
}
|
||||
});
|
||||
|
||||
List<FeedEntryTag> addList = Lists.newArrayList();
|
||||
List<FeedEntryTag> removeList = Lists.newArrayList();
|
||||
|
||||
for (String tagName : tagNames) {
|
||||
FeedEntryTag tag = tagMap.get(tagName);
|
||||
if (tag == null) {
|
||||
addList.add(new FeedEntryTag(user, entry, tagName));
|
||||
}
|
||||
}
|
||||
|
||||
for (FeedEntryTag tag : tags) {
|
||||
if (!tagNames.contains(tag.getName())) {
|
||||
removeList.add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
feedEntryTagDAO.saveOrUpdate(addList);
|
||||
feedEntryTagDAO.delete(removeList);
|
||||
}
|
||||
|
||||
}
|
||||
32
src/main/java/com/commafeed/backend/service/FeedService.java
Normal file
32
src/main/java/com/commafeed/backend/service/FeedService.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
|
||||
import com.commafeed.backend.dao.FeedDAO;
|
||||
import com.commafeed.backend.feed.FeedUtils;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class FeedService {
|
||||
|
||||
private final FeedDAO feedDAO;
|
||||
|
||||
public synchronized Feed findOrCreate(String url) {
|
||||
String normalized = FeedUtils.normalizeURL(url);
|
||||
Feed feed = feedDAO.findByUrl(normalized);
|
||||
if (feed == null) {
|
||||
feed = new Feed();
|
||||
feed.setUrl(url);
|
||||
feed.setNormalizedUrl(normalized);
|
||||
feed.setNormalizedUrlHash(DigestUtils.sha1Hex(normalized));
|
||||
feed.setDisabledUntil(new Date(0));
|
||||
feedDAO.saveOrUpdate(feed);
|
||||
}
|
||||
return feed;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||
import com.commafeed.backend.feed.FeedQueues;
|
||||
import com.commafeed.backend.feed.FeedUtils;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedCategory;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.Models;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.frontend.model.UnreadCount;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class FeedSubscriptionService {
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static class FeedSubscriptionException extends RuntimeException {
|
||||
public FeedSubscriptionException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
private final FeedService feedService;
|
||||
private final FeedQueues queues;
|
||||
private final CacheService cache;
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
public Feed subscribe(User user, String url, String title, FeedCategory category) {
|
||||
|
||||
final String pubUrl = config.getApplicationSettings().getPublicUrl();
|
||||
if (StringUtils.isBlank(pubUrl)) {
|
||||
throw new FeedSubscriptionException("Public URL of this CommaFeed instance is not set");
|
||||
}
|
||||
if (url.startsWith(pubUrl)) {
|
||||
throw new FeedSubscriptionException("Could not subscribe to a feed from this CommaFeed instance");
|
||||
}
|
||||
|
||||
Feed feed = feedService.findOrCreate(url);
|
||||
|
||||
FeedSubscription sub = feedSubscriptionDAO.findByFeed(user, feed);
|
||||
if (sub == null) {
|
||||
sub = new FeedSubscription();
|
||||
sub.setFeed(feed);
|
||||
sub.setUser(user);
|
||||
}
|
||||
sub.setCategory(category);
|
||||
sub.setPosition(0);
|
||||
sub.setTitle(FeedUtils.truncate(title, 128));
|
||||
feedSubscriptionDAO.saveOrUpdate(sub);
|
||||
|
||||
queues.add(feed, false);
|
||||
cache.invalidateUserRootCategory(user);
|
||||
return feed;
|
||||
}
|
||||
|
||||
public boolean unsubscribe(User user, Long subId) {
|
||||
FeedSubscription sub = feedSubscriptionDAO.findById(user, subId);
|
||||
if (sub != null) {
|
||||
feedSubscriptionDAO.delete(sub);
|
||||
cache.invalidateUserRootCategory(user);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshAll(User user) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
for (FeedSubscription sub : subs) {
|
||||
Feed feed = sub.getFeed();
|
||||
queues.add(feed, true);
|
||||
}
|
||||
}
|
||||
|
||||
public UnreadCount getUnreadCount(User user, FeedSubscription sub) {
|
||||
UnreadCount count = cache.getUnreadCount(sub);
|
||||
if (count == null) {
|
||||
log.debug("unread count cache miss for {}", Models.getId(sub));
|
||||
count = feedEntryStatusDAO.getUnreadCount(user, sub);
|
||||
cache.setUnreadCount(sub, count);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public Map<Long, UnreadCount> getUnreadCount(User user) {
|
||||
Map<Long, UnreadCount> map = Maps.newHashMap();
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
for (FeedSubscription sub : subs) {
|
||||
map.put(sub.getId(), getUnreadCount(user, sub));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
|
||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryContent;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class FeedUpdateService {
|
||||
|
||||
private final FeedEntryDAO feedEntryDAO;
|
||||
private final FeedEntryContentService feedEntryContentService;
|
||||
|
||||
/**
|
||||
* this is NOT thread-safe
|
||||
*/
|
||||
public boolean addEntry(Feed feed, FeedEntry entry) {
|
||||
|
||||
Long existing = feedEntryDAO.findExisting(entry.getGuid(), feed);
|
||||
if (existing != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FeedEntryContent content = feedEntryContentService.findOrCreate(entry.getContent(), feed.getLink());
|
||||
entry.setGuidHash(DigestUtils.sha1Hex(entry.getGuid()));
|
||||
entry.setContent(content);
|
||||
entry.setInserted(new Date());
|
||||
entry.setFeed(feed);
|
||||
|
||||
feedEntryDAO.saveOrUpdate(entry);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
60
src/main/java/com/commafeed/backend/service/MailService.java
Normal file
60
src/main/java/com/commafeed/backend/service/MailService.java
Normal file
@@ -0,0 +1,60 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.mail.Authenticator;
|
||||
import javax.mail.Message;
|
||||
import javax.mail.PasswordAuthentication;
|
||||
import javax.mail.Session;
|
||||
import javax.mail.Transport;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
import javax.mail.internet.MimeMessage;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.CommaFeedConfiguration.ApplicationSettings;
|
||||
import com.commafeed.backend.model.User;
|
||||
|
||||
/**
|
||||
* Mailing service
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class MailService implements Serializable {
|
||||
|
||||
@Inject
|
||||
CommaFeedConfiguration config;
|
||||
|
||||
public void sendMail(User user, String subject, String content) throws Exception {
|
||||
|
||||
ApplicationSettings settings = config.getApplicationSettings();
|
||||
|
||||
final String username = settings.getSmtpUserName();
|
||||
final String password = settings.getSmtpPassword();
|
||||
|
||||
String dest = user.getEmail();
|
||||
|
||||
Properties props = new Properties();
|
||||
props.put("mail.smtp.auth", "true");
|
||||
props.put("mail.smtp.starttls.enable", "" + settings.isSmtpTls());
|
||||
props.put("mail.smtp.host", settings.getSmtpHost());
|
||||
props.put("mail.smtp.port", "" + settings.getSmtpPort());
|
||||
|
||||
Session session = Session.getInstance(props, new Authenticator() {
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
return new PasswordAuthentication(username, password);
|
||||
}
|
||||
});
|
||||
|
||||
Message message = new MimeMessage(session);
|
||||
message.setFrom(new InternetAddress(username, "CommaFeed"));
|
||||
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(dest));
|
||||
message.setSubject("CommaFeed - " + subject);
|
||||
message.setContent(content, "text/html; charset=utf-8");
|
||||
|
||||
Transport.send(message);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
// taken from http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html
|
||||
@SuppressWarnings("serial")
|
||||
@Slf4j
|
||||
public class PasswordEncryptionService implements Serializable {
|
||||
|
||||
public boolean authenticate(String attemptedPassword, byte[] encryptedPassword, byte[] salt) {
|
||||
// Encrypt the clear-text password using the same salt that was used to
|
||||
// encrypt the original password
|
||||
byte[] encryptedAttemptedPassword = null;
|
||||
try {
|
||||
encryptedAttemptedPassword = getEncryptedPassword(attemptedPassword, salt);
|
||||
} catch (Exception e) {
|
||||
// should never happen
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
|
||||
// Authentication succeeds if encrypted password that the user entered
|
||||
// is equal to the stored hash
|
||||
return Arrays.equals(encryptedPassword, encryptedAttemptedPassword);
|
||||
}
|
||||
|
||||
public byte[] getEncryptedPassword(String password, byte[] salt) {
|
||||
// PBKDF2 with SHA-1 as the hashing algorithm. Note that the NIST
|
||||
// specifically names SHA-1 as an acceptable hashing algorithm for
|
||||
// PBKDF2
|
||||
String algorithm = "PBKDF2WithHmacSHA1";
|
||||
// SHA-1 generates 160 bit hashes, so that's what makes sense here
|
||||
int derivedKeyLength = 160;
|
||||
// Pick an iteration count that works for you. The NIST recommends at
|
||||
// least 1,000 iterations:
|
||||
// http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf
|
||||
// iOS 4.x reportedly uses 10,000:
|
||||
// http://blog.crackpassword.com/2010/09/smartphone-forensics-cracking-blackberry-backup-passwords/
|
||||
int iterations = 20000;
|
||||
|
||||
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, derivedKeyLength);
|
||||
|
||||
byte[] bytes = null;
|
||||
try {
|
||||
SecretKeyFactory f = SecretKeyFactory.getInstance(algorithm);
|
||||
SecretKey key = f.generateSecret(spec);
|
||||
bytes = key.getEncoded();
|
||||
} catch (Exception e) {
|
||||
// should never happen
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public byte[] generateSalt() {
|
||||
// VERY important to use SecureRandom instead of just Random
|
||||
|
||||
byte[] salt = null;
|
||||
try {
|
||||
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
|
||||
|
||||
// Generate a 8 byte (64 bit) salt as recommended by RSA PKCS5
|
||||
salt = new byte[8];
|
||||
random.nextBytes(salt);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// should never happen
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
return salt;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
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 com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.HttpGetter;
|
||||
import com.commafeed.backend.feed.FeedQueues;
|
||||
import com.commafeed.backend.feed.FeedUtils;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.frontend.resource.PubSubHubbubCallbackREST;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
/**
|
||||
* Sends push subscription requests. Callback is handled by {@link PubSubHubbubCallbackREST}
|
||||
*
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class PubSubService {
|
||||
|
||||
private final CommaFeedConfiguration config;
|
||||
private final FeedQueues queues;
|
||||
|
||||
public void subscribe(Feed feed) {
|
||||
|
||||
try {
|
||||
// make sure the feed has been updated in the database so that the
|
||||
// callback works
|
||||
Thread.sleep(30000);
|
||||
} catch (InterruptedException e1) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
String hub = feed.getPushHub();
|
||||
String topic = feed.getPushTopic();
|
||||
String publicUrl = FeedUtils.removeTrailingSlash(config.getApplicationSettings().getPublicUrl());
|
||||
|
||||
log.debug("sending new pubsub subscription to {} for {}", hub, topic);
|
||||
|
||||
HttpPost post = new HttpPost(hub);
|
||||
List<NameValuePair> nvp = Lists.newArrayList();
|
||||
nvp.add(new BasicNameValuePair("hub.callback", publicUrl + "/rest/push/callback"));
|
||||
nvp.add(new BasicNameValuePair("hub.topic", topic));
|
||||
nvp.add(new BasicNameValuePair("hub.mode", "subscribe"));
|
||||
nvp.add(new BasicNameValuePair("hub.verify", "async"));
|
||||
nvp.add(new BasicNameValuePair("hub.secret", ""));
|
||||
nvp.add(new BasicNameValuePair("hub.verify_token", ""));
|
||||
nvp.add(new BasicNameValuePair("hub.lease_seconds", ""));
|
||||
|
||||
post.setHeader(HttpHeaders.USER_AGENT, "CommaFeed");
|
||||
post.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
|
||||
|
||||
CloseableHttpClient client = HttpGetter.newClient(20000);
|
||||
CloseableHttpResponse response = null;
|
||||
try {
|
||||
post.setEntity(new UrlEncodedFormEntity(nvp));
|
||||
response = client.execute(post);
|
||||
|
||||
int code = response.getStatusLine().getStatusCode();
|
||||
if (code != 204 && code != 202 && code != 200) {
|
||||
String message = EntityUtils.toString(response.getEntity());
|
||||
String pushpressError = " is value is not allowed. You may only subscribe to";
|
||||
if (code == 400 && StringUtils.contains(message, pushpressError)) {
|
||||
String[] tokens = message.split(" ");
|
||||
feed.setPushTopic(tokens[tokens.length - 1]);
|
||||
queues.giveBack(feed);
|
||||
log.debug("handled pushpress subfeed {} : {}", topic, feed.getPushTopic());
|
||||
} else {
|
||||
throw new Exception("Unexpected response code: " + code + " " + response.getStatusLine().getReasonPhrase() + " - "
|
||||
+ message);
|
||||
}
|
||||
}
|
||||
log.debug("subscribed to {} for {}", hub, topic);
|
||||
} catch (Exception e) {
|
||||
log.error("Could not subscribe to {} for {} : " + e.getMessage(), hub, topic);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(response);
|
||||
IOUtils.closeQuietly(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
127
src/main/java/com/commafeed/backend/service/StartupService.java
Normal file
127
src/main/java/com/commafeed/backend/service/StartupService.java
Normal file
@@ -0,0 +1,127 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.naming.Context;
|
||||
import javax.naming.InitialContext;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import liquibase.Liquibase;
|
||||
import liquibase.database.Database;
|
||||
import liquibase.database.DatabaseFactory;
|
||||
import liquibase.database.core.PostgresDatabase;
|
||||
import liquibase.database.jvm.JdbcConnection;
|
||||
import liquibase.resource.ClassLoaderResourceAccessor;
|
||||
import liquibase.resource.ResourceAccessor;
|
||||
import liquibase.structure.DatabaseObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl;
|
||||
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
|
||||
import org.hibernate.internal.SessionFactoryImpl;
|
||||
|
||||
import com.commafeed.CommaFeedApplication;
|
||||
import com.commafeed.backend.dao.UnitOfWork;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
|
||||
@Slf4j
|
||||
public class StartupService implements Managed {
|
||||
|
||||
private SessionFactory sessionFactory;
|
||||
private UserDAO userDAO;
|
||||
private UserService userService;
|
||||
|
||||
public StartupService(SessionFactory sessionFactory, UserDAO userDAO, UserService userService) {
|
||||
this.sessionFactory = sessionFactory;
|
||||
this.userDAO = userDAO;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws Exception {
|
||||
updateSchema();
|
||||
new UnitOfWork<Void>(sessionFactory) {
|
||||
@Override
|
||||
protected Void runInSession() throws Exception {
|
||||
if (userDAO.findByName(CommaFeedApplication.USERNAME_ADMIN) == null) {
|
||||
initialData();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.run();
|
||||
}
|
||||
|
||||
private void updateSchema() {
|
||||
try {
|
||||
Context context = null;
|
||||
Connection connection = null;
|
||||
try {
|
||||
Thread currentThread = Thread.currentThread();
|
||||
ClassLoader classLoader = currentThread.getContextClassLoader();
|
||||
ResourceAccessor accessor = new ClassLoaderResourceAccessor(classLoader);
|
||||
|
||||
context = new InitialContext();
|
||||
DataSource dataSource = getDataSource(sessionFactory);
|
||||
connection = dataSource.getConnection();
|
||||
JdbcConnection jdbcConnection = new JdbcConnection(connection);
|
||||
|
||||
Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(jdbcConnection);
|
||||
|
||||
if (database instanceof PostgresDatabase) {
|
||||
database = new PostgresDatabase() {
|
||||
@Override
|
||||
public String escapeObjectName(String objectName, Class<? extends DatabaseObject> objectType) {
|
||||
return objectName;
|
||||
}
|
||||
};
|
||||
database.setConnection(jdbcConnection);
|
||||
}
|
||||
|
||||
Liquibase liq = new Liquibase("migrations.xml", accessor, database);
|
||||
liq.update("prod");
|
||||
} finally {
|
||||
if (context != null) {
|
||||
context.close();
|
||||
}
|
||||
if (connection != null) {
|
||||
connection.close();
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void initialData() {
|
||||
log.info("Populating database with default values");
|
||||
try {
|
||||
userService.register(CommaFeedApplication.USERNAME_ADMIN, "admin", "admin@commafeed.com", Arrays.asList(Role.ADMIN, Role.USER),
|
||||
true);
|
||||
userService.register(CommaFeedApplication.USERNAME_DEMO, "demo", "demo@commafeed.com", Arrays.asList(Role.USER), true);
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
|
||||
}
|
||||
|
||||
private static DataSource getDataSource(SessionFactory sessionFactory) {
|
||||
if (sessionFactory instanceof SessionFactoryImpl) {
|
||||
ConnectionProvider cp = ((SessionFactoryImpl) sessionFactory).getConnectionProvider();
|
||||
if (cp instanceof DatasourceConnectionProviderImpl) {
|
||||
return ((DatasourceConnectionProviderImpl) cp).getDataSource();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
117
src/main/java/com/commafeed/backend/service/UserService.java
Normal file
117
src/main/java/com/commafeed/backend/service/UserService.java
Normal file
@@ -0,0 +1,117 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.lang.time.DateUtils;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.dao.UserSettingsDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class UserService {
|
||||
|
||||
private final FeedCategoryDAO feedCategoryDAO;
|
||||
private final UserDAO userDAO;
|
||||
private final UserSettingsDAO userSettingsDAO;
|
||||
|
||||
private final FeedSubscriptionService feedSubscriptionService;
|
||||
private final PasswordEncryptionService encryptionService;
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
public User login(String name, String password) {
|
||||
if (name == null || password == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
User user = userDAO.findByName(name);
|
||||
if (user != null && !user.isDisabled()) {
|
||||
boolean authenticated = encryptionService.authenticate(password, user.getPassword(), user.getSalt());
|
||||
if (authenticated) {
|
||||
Date lastLogin = user.getLastLogin();
|
||||
Date now = new Date();
|
||||
|
||||
boolean saveUser = false;
|
||||
// only update lastLogin field every hour in order to not
|
||||
// invalidate the cache everytime someone logs in
|
||||
if (lastLogin == null || lastLogin.before(DateUtils.addHours(now, -1))) {
|
||||
user.setLastLogin(now);
|
||||
saveUser = true;
|
||||
}
|
||||
if (config.getApplicationSettings().isHeavyLoad()
|
||||
&& (user.getLastFullRefresh() == null || user.getLastFullRefresh().before(DateUtils.addMinutes(now, -30)))) {
|
||||
user.setLastFullRefresh(now);
|
||||
saveUser = true;
|
||||
feedSubscriptionService.refreshAll(user);
|
||||
}
|
||||
if (saveUser) {
|
||||
userDAO.saveOrUpdate(user);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public User register(String name, String password, String email, Collection<Role> roles) {
|
||||
return register(name, password, email, roles, false);
|
||||
}
|
||||
|
||||
public User register(String name, String password, String email, Collection<Role> roles, boolean forceRegistration) {
|
||||
|
||||
Preconditions.checkNotNull(name);
|
||||
Preconditions.checkArgument(StringUtils.length(name) <= 32, "Name too long (32 characters maximum)");
|
||||
Preconditions.checkNotNull(password);
|
||||
|
||||
if (!forceRegistration) {
|
||||
Preconditions.checkState(config.getApplicationSettings().isAllowRegistrations(),
|
||||
"Registrations are closed on this CommaFeed instance");
|
||||
|
||||
Preconditions.checkNotNull(email);
|
||||
Preconditions.checkArgument(StringUtils.length(name) >= 3, "Name too short (3 characters minimum)");
|
||||
Preconditions
|
||||
.checkArgument(forceRegistration || StringUtils.length(password) >= 6, "Password too short (6 characters maximum)");
|
||||
Preconditions.checkArgument(StringUtils.contains(email, "@"), "Invalid email address");
|
||||
}
|
||||
|
||||
Preconditions.checkArgument(userDAO.findByName(name) == null, "Name already taken");
|
||||
if (StringUtils.isNotBlank(email)) {
|
||||
Preconditions.checkArgument(userDAO.findByEmail(email) == null, "Email already taken");
|
||||
}
|
||||
|
||||
User user = new User();
|
||||
byte[] salt = encryptionService.generateSalt();
|
||||
user.setName(name);
|
||||
user.setEmail(email);
|
||||
user.setCreated(new Date());
|
||||
user.setSalt(salt);
|
||||
user.setPassword(encryptionService.getEncryptedPassword(password, salt));
|
||||
for (Role role : roles) {
|
||||
user.getRoles().add(new UserRole(user, role));
|
||||
}
|
||||
userDAO.saveOrUpdate(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
public void unregister(User user) {
|
||||
feedCategoryDAO.delete(feedCategoryDAO.findAll(user));
|
||||
userSettingsDAO.delete(userSettingsDAO.findByUser(user));
|
||||
userDAO.delete(user);
|
||||
}
|
||||
|
||||
public String generateApiKey(User user) {
|
||||
byte[] key = encryptionService.getEncryptedPassword(UUID.randomUUID().toString(), user.getSalt());
|
||||
return DigestUtils.sha1Hex(key);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user