initial commit

This commit is contained in:
Athou
2013-03-20 20:33:42 +01:00
commit 7b3c53fcb9
82 changed files with 3346 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
package com.commafeed.backend;
import javax.annotation.PostConstruct;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.inject.Inject;
import com.commafeed.backend.dao.FeedCategoryService;
import com.commafeed.backend.dao.FeedService;
import com.commafeed.backend.dao.FeedSubscriptionService;
import com.commafeed.backend.dao.UserService;
import com.commafeed.backend.security.PasswordEncryptionService;
import com.commafeed.model.Feed;
import com.commafeed.model.FeedCategory;
import com.commafeed.model.FeedSubscription;
import com.commafeed.model.User;
@Startup
@Singleton
public class StartupBean {
@Inject
FeedService feedService;
@Inject
FeedCategoryService feedCategoryService;
@Inject
FeedSubscriptionService feedSubscriptionService;
@Inject
UserService userService;
@Inject
PasswordEncryptionService encryptionService;
@PostConstruct
private void init() {
if (userService.getCount() == 0) {
User user = new User();
byte[] salt = encryptionService.generateSalt();
user.setName("admin");
user.setSalt(salt);
user.setPassword(encryptionService.getEncryptedPassword("admin",
salt));
userService.save(user);
Feed feed = new Feed("http://feed.dilbert.com/dilbert/daily_strip");
feedService.save(feed);
FeedCategory newsCategory = new FeedCategory();
newsCategory.setName("News");
newsCategory.setUser(user);
feedCategoryService.save(newsCategory);
FeedCategory comicsCategory = new FeedCategory();
comicsCategory.setName("Comics");
comicsCategory.setUser(user);
comicsCategory.setParent(newsCategory);
feedCategoryService.save(comicsCategory);
FeedSubscription sub = new FeedSubscription();
sub.setCategory(comicsCategory);
sub.setFeed(feed);
sub.setTitle("Dilbert - Strips");
sub.setUser(user);
feedSubscriptionService.save(sub);
}
}
}

View File

@@ -0,0 +1,17 @@
package com.commafeed.backend.dao;
import java.util.List;
import javax.ejb.Stateless;
import com.commafeed.frontend.utils.ModelFactory.MF;
import com.commafeed.model.FeedCategory;
import com.commafeed.model.User;
@Stateless
public class FeedCategoryService extends GenericDAO<FeedCategory, Long> {
public List<FeedCategory> findAll(User user) {
return findByField(MF.i(MF.p(FeedCategory.class).getUser()), user);
}
}

View File

@@ -0,0 +1,31 @@
package com.commafeed.backend.dao;
import java.util.Calendar;
import java.util.Collection;
import javax.ejb.Stateless;
import javax.inject.Inject;
import com.commafeed.model.Feed;
import com.commafeed.model.FeedEntry;
@Stateless
public class FeedEntryService extends GenericDAO<FeedEntry, String> {
@Inject
FeedService feedService;
public void updateEntries(String url, Collection<FeedEntry> entries) {
Feed feed = feedService.findById(url);
for (FeedEntry entry : entries) {
FeedEntry existing = findById(entry.getGuid());
if (existing == null) {
entry.setFeed(feed);
save(entry);
}
}
feed.setLastUpdated(Calendar.getInstance().getTime());
em.merge(feed);
}
}

View File

@@ -0,0 +1,10 @@
package com.commafeed.backend.dao;
import javax.ejb.Stateless;
import com.commafeed.model.Feed;
@Stateless
public class FeedService extends GenericDAO<Feed, String> {
}

View File

@@ -0,0 +1,18 @@
package com.commafeed.backend.dao;
import java.util.List;
import javax.ejb.Stateless;
import com.commafeed.frontend.utils.ModelFactory.MF;
import com.commafeed.model.FeedCategory;
import com.commafeed.model.FeedSubscription;
import com.commafeed.model.User;
@Stateless
public class FeedSubscriptionService extends GenericDAO<FeedSubscription, Long> {
public List<FeedSubscription> findAll(User user) {
return findByField(MF.i(MF.p(FeedCategory.class).getUser()), user);
}
}

View File

@@ -0,0 +1,89 @@
package com.commafeed.backend.dao;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import com.google.common.reflect.TypeToken;
import com.uaihebert.factory.EasyCriteriaFactory;
import com.uaihebert.model.EasyCriteria;
public abstract class GenericDAO<T, K> {
private TypeToken<T> type = new TypeToken<T>(getClass()) {
};
@PersistenceContext
protected EntityManager em;
protected CriteriaBuilder builder;
@PostConstruct
public void init() {
builder = em.getCriteriaBuilder();
}
public void save(T object) {
em.persist(object);
}
public void update(T... objects) {
for (Object object : objects) {
em.merge(object);
}
}
public void delete(T object) {
object = em.merge(object);
em.remove(object);
}
public void deleteById(Object id) {
Object ref = em.getReference(getType(), id);
em.remove(ref);
}
public T findById(K id) {
T t = em.find(getType(), id);
return t;
}
public List<T> findAll() {
return EasyCriteriaFactory.createQueryCriteria(em, getType())
.getResultList();
}
public List<T> findAll(int startIndex, int count) {
EasyCriteria<T> criteria = EasyCriteriaFactory.createQueryCriteria(em,
getType());
criteria.setMaxResults(count);
criteria.setFirstResult(startIndex);
return criteria.getResultList();
}
public long getCount() {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Long> query = builder.createQuery(Long.class);
Root<T> root = query.from(getType());
query.select(builder.count(root));
return em.createQuery(query).getSingleResult();
}
public List<T> findByField(String field, Object value) {
EasyCriteria<T> criteria = EasyCriteriaFactory.createQueryCriteria(em,
getType());
criteria.andEquals(field, value);
return criteria.getResultList();
}
@SuppressWarnings("unchecked")
protected Class<T> getType() {
return (Class<T>) type.getRawType();
}
}

View File

@@ -0,0 +1,30 @@
package com.commafeed.backend.dao;
import java.util.List;
import javax.inject.Inject;
import com.commafeed.backend.security.PasswordEncryptionService;
import com.commafeed.frontend.utils.ModelFactory.MF;
import com.commafeed.model.User;
import com.google.common.collect.Iterables;
public class UserService extends GenericDAO<User, Long> {
@Inject
PasswordEncryptionService encryptionService;
public User login(String name, String password) {
List<User> users = findByField(MF.i(MF.p(User.class).getName()), name);
User user = Iterables.getFirst(users, null);
if (user != null) {
boolean authenticated = encryptionService.authenticate(password,
user.getPassword(), user.getSalt());
if (authenticated) {
return user;
}
}
return null;
}
}

View File

@@ -0,0 +1,53 @@
package com.commafeed.backend.feeds;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.ejb.AccessTimeout;
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.Lock;
import javax.ejb.LockType;
import javax.ejb.Singleton;
import javax.inject.Inject;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.model.Feed;
@Singleton
public class FeedFetcher {
private static Logger log = LoggerFactory.getLogger(FeedFetcher.class);
@Inject
FeedParser parser;
@Asynchronous
@Lock(LockType.READ)
@AccessTimeout(value = 15, unit = TimeUnit.SECONDS)
public Future<Feed> fetch(String feedUrl) {
log.debug("Fetching feed {}", feedUrl);
Feed feed = null;
HttpClient httpclient = new DefaultHttpClient();
try {
HttpGet httpget = new HttpGet(feedUrl);
ResponseHandler<String> responseHandler = new BasicResponseHandler();
String responseBody = httpclient.execute(httpget, responseHandler);
feed = parser.parse(feedUrl, responseBody);
} catch (Exception e) {
e.printStackTrace();
} finally {
httpclient.getConnectionManager().shutdown();
}
return new AsyncResult<Feed>(feed);
}
}

View File

@@ -0,0 +1,49 @@
package com.commafeed.backend.feeds;
import java.io.StringReader;
import java.util.Calendar;
import java.util.List;
import javax.ejb.Stateless;
import com.commafeed.model.Feed;
import com.commafeed.model.FeedEntry;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedInput;
@Stateless
public class FeedParser {
@SuppressWarnings("unchecked")
public Feed parse(String feedUrl, String xml) throws FeedException {
Feed feed = new Feed();
feed.setUrl(feedUrl);
feed.setLastUpdated(Calendar.getInstance().getTime());
try {
SyndFeed rss = new SyndFeedInput().build(new StringReader(xml));
List<SyndEntry> items = rss.getEntries();
for (SyndEntry item : items) {
FeedEntry entry = new FeedEntry();
entry.setGuid(item.getUri());
entry.setTitle(item.getTitle());
entry.setContent(item.getDescription() == null ? null : item
.getDescription().getValue());
entry.setUrl(item.getLink());
entry.setUpdated(item.getUpdatedDate() != null ? item
.getUpdatedDate() : item.getPublishedDate());
feed.getEntries().add(entry);
}
} catch (Exception e) {
throw new FeedException("Could not parse feed : " + e.getMessage(),
e);
}
return feed;
}
}

View File

@@ -0,0 +1,48 @@
package com.commafeed.backend.feeds;
import java.util.List;
import java.util.concurrent.Future;
import javax.ejb.Schedule;
import javax.ejb.Singleton;
import javax.inject.Inject;
import com.commafeed.backend.dao.FeedEntryService;
import com.commafeed.backend.dao.FeedService;
import com.commafeed.model.Feed;
import com.google.common.collect.Lists;
@Singleton
public class FeedTimer {
@Inject
FeedService feedService;
@Inject
FeedEntryService feedEntryService;
@Inject
FeedFetcher fetcher;
@Schedule(hour = "*", minute = "*", persistent = false)
private void timeout() {
List<Feed> feeds = feedService.findAll();
List<Future<Feed>> futures = Lists.newArrayList();
for (Feed feed : feeds) {
Future<Feed> future = fetcher.fetch(feed.getUrl());
futures.add(future);
}
for (Future<Feed> future : futures) {
try {
Feed feed = future.get();
feedEntryService
.updateEntries(feed.getUrl(), feed.getEntries());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

View File

@@ -0,0 +1,89 @@
package com.commafeed.backend.security;
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 javax.ejb.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.dao.UserService;
// http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html
@Singleton
public class PasswordEncryptionService {
private static final Logger log = LoggerFactory
.getLogger(UserService.class);
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);
SecretKey key = null;
try {
SecretKeyFactory f = SecretKeyFactory.getInstance(algorithm);
key = f.generateSecret(spec);
} catch (Exception e) {
// should never happen
log.error(e.getMessage(), e);
}
return key.getEncoded();
}
public byte[] generateSalt() {
// VERY important to use SecureRandom instead of just Random
SecureRandom random = null;
try {
random = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
// should never happen
log.error(e.getMessage(), e);
}
// Generate a 8 byte (64 bit) salt as recommended by RSA PKCS5
byte[] salt = new byte[8];
random.nextBytes(salt);
return salt;
}
}