[haiku-depot-web] [haiku-depot-web-app] push by haiku.li...@xxxxxxxxx - implement atom feed on 2014-08-14 11:11 GMT

  • From: haiku-depot-web-app@xxxxxxxxxxxxxx
  • To: haiku-depot-web@xxxxxxxxxxxxx
  • Date: Thu, 14 Aug 2014 11:11:21 +0000

Revision: 761e4b024d1f
Author:   Andrew Lindesay <apl@xxxxxxxxxxxxxx>
Date:     Thu Aug 14 11:03:33 2014 UTC
Log:      implement atom feed

http://code.google.com/p/haiku-depot-web-app/source/detail?r=761e4b024d1f

Added:
/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GenerateFeedUrlRequest.java /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GenerateFeedUrlResult.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/feed/FeedOrchestrationService.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/feed/controller/FeedController.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/feed/model/FeedSpecification.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/feed/model/SyndEntrySupplier.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/CreatedPkgVersionSyndEntrySupplier.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/web/ErrorServlet.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/userrating/CreatedUserRatingSyndEntrySupplier.java
 /haikudepotserver-webapp/src/main/webapp/css/pkgfeedbuilder.css
 /haikudepotserver-webapp/src/main/webapp/img/feed.svg
/haikudepotserver-webapp/src/main/webapp/js/app/controller/pkgfeedbuilder.html /haikudepotserver-webapp/src/main/webapp/js/app/controller/pkgfeedbuildercontroller.js
Deleted:
/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/web/ErrorFilter.java
Modified:
/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApi.java
 /haikudepotserver-docs/src/main/latex/docs/part-config.tex
 /haikudepotserver-docs/src/main/latex/docs/part-localization.tex
 /haikudepotserver-parent/pom.xml
 /haikudepotserver-rpm/src/main/etc/config__config.properties
 /haikudepotserver-webapp/pom.xml
/haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApiImpl.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/PkgVersion.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/passwordreset/PasswordResetOrchestrationService.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/PkgOrchestrationService.java
 /haikudepotserver-webapp/src/main/resources/messages.properties
 /haikudepotserver-webapp/src/main/resources/messages_de.properties
 /haikudepotserver-webapp/src/main/resources/spring/general.xml
 /haikudepotserver-webapp/src/main/webapp/WEB-INF/web.xml
 /haikudepotserver-webapp/src/main/webapp/css/viewpkg.css
 /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkg.html
/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js
 /haikudepotserver-webapp/src/main/webapp/js/app/directive/banner.html
/haikudepotserver-webapp/src/main/webapp/js/app/directive/bannerdirective.js
 /haikudepotserver-webapp/src/main/webapp/js/app/routes.js
/haikudepotserver-webapp/src/main/webapp/js/app/service/breadcrumbfactoryservice.js /haikudepotserver-webapp/src/main/webapp/js/app/service/referencedataservice.js /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/api1/MiscelaneousApiIT.java
 /haikudepotserver-webapp/src/test/resources/local.properties

=======================================
--- /dev/null
+++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GenerateFeedUrlRequest.java Thu Aug 14 11:03:33 2014 UTC
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.api1.model.miscellaneous;
+
+import java.util.List;
+
+public class GenerateFeedUrlRequest {
+
+    public enum SupplierType {
+
+        /**
+         * <p>Provide feed entries from creations of package versions.</p>
+         */
+
+        CREATEDPKGVERSION,
+
+        /**
+         * <p>Provide feed entries from creations of user ratings.</p>
+         */
+
+        CREATEDUSERRATING
+    };
+
+    /**
+ * <p>If possible, the content may be localized. In this case, the preference for the language
+     * is made by supplying the natural language code.</p>
+     */
+
+    public String naturalLanguageCode;
+
+    /**
+ * <p>The package names for which the feed may be generated are specified with this. If the list + * is empty then the feed will be empty. If the list is null then the feed will draw from all
+     * of the packages.</p>
+     */
+
+    public List<String> pkgNames;
+
+    /**
+ * <p>This is the limit to the number of entries that will be provided by a given feed. The + * feed may have an absolute limit as well; so you may ask for X, but only be provided with
+     * less than X items because of the absolute limit.</p>
+     */
+    public Integer limit;
+
+    /**
+ * <p>These are essentially the sources from which the feed will be sourced.</p>
+     */
+
+    public List<SupplierType> supplierTypes;
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/miscellaneous/GenerateFeedUrlResult.java Thu Aug 14 11:03:33 2014 UTC
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.api1.model.miscellaneous;
+
+public class GenerateFeedUrlResult {
+
+    public String url;
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/feed/FeedOrchestrationService.java Thu Aug 14 11:03:33 2014 UTC
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.feed;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+import org.haikuos.haikudepotserver.feed.controller.FeedController;
+import org.haikuos.haikudepotserver.feed.model.FeedSpecification;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.util.UriComponentsBuilder;
+
+@Service
+public class FeedOrchestrationService {
+
+    @Value("${baseurl}")
+    String baseUrl;
+
+    /**
+ * <p>Given a specification for a feed, this method will generate a URL that external users can query in order
+     * to get that feed.</p>
+     */
+
+    public String generateUrl(FeedSpecification specification) {
+        Preconditions.checkNotNull(specification);
+
+        UriComponentsBuilder builder = UriComponentsBuilder
+                .fromHttpUrl(baseUrl)
+ .path(FeedController.PATH_ROOT + FeedController.PATH_PKG_LEAF);
+
+        if(null!=specification.getNaturalLanguageCode()) {
+ builder.queryParam(FeedController.KEY_NATURALLANGUAGECODE, specification.getNaturalLanguageCode());
+        }
+
+        if(null!=specification.getLimit()) {
+ builder.queryParam(FeedController.KEY_LIMIT, specification.getLimit().toString());
+        }
+
+        if(null!=specification.getSupplierTypes()) {
+ builder.queryParam(FeedController.KEY_TYPES, Joiner.on(',').join(Iterables.transform(
+                    specification.getSupplierTypes(),
+ new Function<FeedSpecification.SupplierType, Object>() {
+                        @Override
+ public Object apply(FeedSpecification.SupplierType input) {
+                            return input.name();
+                        }
+                    }
+            )));
+        }
+
+        if(null!=specification.getPkgNames()) {
+ // split on hyphens because hyphens are not allowed in package names + builder.queryParam(FeedController.KEY_PKGNAMES, Joiner.on('-').join(specification.getPkgNames()));
+        }
+
+        return builder.build().toString();
+    }
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/feed/controller/FeedController.java Thu Aug 14 11:03:33 2014 UTC
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.feed.controller;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.net.MediaType;
+import com.sun.syndication.feed.synd.*;
+import com.sun.syndication.io.FeedException;
+import com.sun.syndication.io.SyndFeedOutput;
+import org.haikuos.haikudepotserver.dataobjects.NaturalLanguage;
+import org.haikuos.haikudepotserver.feed.model.FeedSpecification;
+import org.haikuos.haikudepotserver.feed.model.SyndEntrySupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * <p>This controller produces an ATOM feed of the latest happenings </p>
+ */
+
+@Controller
+@RequestMapping(FeedController.PATH_ROOT)
+public class FeedController {
+
+ protected static Logger LOGGER = LoggerFactory.getLogger(FeedController.class);
+
+    public final static String KEY_NATURALLANGUAGECODE = "natlangcode";
+    public final static String KEY_PKGNAMES = "pkgnames";
+    public final static String KEY_LIMIT = "limit";
+    public final static String KEY_TYPES = "types";
+
+    public final static String FEEDTYPE = "atom_1.0";
+    public final static String FEEDTITLE = "Haiku Depot Server Feed";
+
+    public final static int DEFAULT_LIMIT = 50;
+    public final static int MAX_LIMIT = 100;
+
+    public final static long EXPIRY_CACHE_SECONDS = 60;
+
+    public final static String PATH_ROOT = "/feed";
+    public final static String PATH_PKG_LEAF = "/pkg.atom";
+
+    @Resource
+    List<SyndEntrySupplier> syndEntrySuppliers;
+
+    @Value("${baseurl}")
+    String baseUrl;
+
+ private LoadingCache<FeedSpecification,SyndFeed> feedCache = CacheBuilder
+            .newBuilder()
+            .maximumSize(10)
+            .expireAfterWrite(EXPIRY_CACHE_SECONDS, TimeUnit.SECONDS)
+            .build(new CacheLoader<FeedSpecification, SyndFeed>() {
+                @Override
+ public SyndFeed load(FeedSpecification key) throws Exception {
+                    Preconditions.checkNotNull(key);
+
+                    SyndFeed feed = new SyndFeedImpl();
+                    feed.setFeedType(FEEDTYPE);
+                    feed.setTitle(FEEDTITLE);
+                    feed.setLink(baseUrl);
+                    feed.setPublishedDate(new java.util.Date());
+
+                    SyndImage image = new SyndImageImpl();
+                    image.setUrl(baseUrl + "/img/haikudepot32.png");
+                    feed.setImage(image);
+
+                    List<SyndEntry> entries = Lists.newArrayList();
+
+                    for(SyndEntrySupplier supplier : syndEntrySuppliers) {
+                        entries.addAll(supplier.generate(key));
+                    }
+
+ // sort the entries and then take the first number of them up to the limit.
+
+                    Collections.sort(entries, new Comparator<SyndEntry>() {
+                        @Override
+                        public int compare(SyndEntry o1, SyndEntry o2) {
+ return -1 * o1.getPublishedDate().compareTo(o2.getPublishedDate());
+                        }
+                    });
+
+                    if(entries.size() > key.getLimit()) {
+                        entries = entries.subList(0,key.getLimit());
+                    }
+
+                    feed.setEntries(entries);
+
+                    return feed;
+                }
+            });
+
+    @RequestMapping(value = PATH_PKG_LEAF, method = RequestMethod.GET)
+    public void generate(
+            HttpServletResponse response,
+ @RequestParam(value = KEY_NATURALLANGUAGECODE, required = false) String naturalLanguageCode, + @RequestParam(value = KEY_PKGNAMES, required = false) String pkgNames, + @RequestParam(value = KEY_LIMIT, required = false) Integer limit, + @RequestParam(value = KEY_TYPES, required = false) String types) throws IOException, FeedException {
+
+        Preconditions.checkNotNull(response);
+
+        if(null==limit || limit.intValue() > MAX_LIMIT) {
+            limit = DEFAULT_LIMIT;
+        }
+
+        FeedSpecification specification = new FeedSpecification();
+ specification.setLimit(null==limit ? DEFAULT_LIMIT : limit.intValue() > MAX_LIMIT ? MAX_LIMIT : limit.intValue()); + specification.setNaturalLanguageCode(!Strings.isNullOrEmpty(naturalLanguageCode) ? naturalLanguageCode : NaturalLanguage.CODE_ENGLISH);
+
+        if(Strings.isNullOrEmpty(types)) {
+ specification.setSupplierTypes(ImmutableList.copyOf(FeedSpecification.SupplierType.values()));
+        }
+        else {
+            specification.setSupplierTypes(Lists.transform(
+ Splitter.on(',').trimResults().omitEmptyStrings().splitToList(types), + new Function<String, FeedSpecification.SupplierType>() {
+                        @Override
+ public FeedSpecification.SupplierType apply(String input) { + return FeedSpecification.SupplierType.valueOf(input);
+                        }
+                    }
+            ));
+        }
+
+        if(Strings.isNullOrEmpty(pkgNames)) {
+            specification.setPkgNames(null);
+        }
+        else {
+ // split on hyphens because hyphens are not allowed in package names + specification.setPkgNames(Splitter.on('-').trimResults().omitEmptyStrings().splitToList(pkgNames));
+        }
+
+        SyndFeed feed = feedCache.getUnchecked(specification);
+
+        response.setContentType(MediaType.ATOM_UTF_8.toString());
+
+        Writer writer = response.getWriter();
+        SyndFeedOutput syndFeedOutput = new SyndFeedOutput();
+        syndFeedOutput.output(feed, writer);
+
+        writer.close();
+    }
+
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/feed/model/FeedSpecification.java Thu Aug 14 11:03:33 2014 UTC
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.feed.model;
+
+import java.util.List;
+
+/**
+ * <p>This class defines the specification of a feed (rss) from this system.</p>
+ *
+ * <p>This is unlike other models in that it specifies elements of the system not by their data objects, + * but their natural references such as package name or natural language code. This is because there + * may be a number of packages involved and the system should not fault them all in order to produce an
+ * RSS feed.</p>
+ */
+
+public class FeedSpecification {
+
+    public enum SupplierType {
+        CREATEDPKGVERSION,
+        CREATEDUSERRATING
+    };
+
+    private String naturalLanguageCode;
+    private List<String> pkgNames;
+    private Integer limit;
+    private List<SupplierType> supplierTypes;
+
+    public String getNaturalLanguageCode() {
+        return naturalLanguageCode;
+    }
+
+    public void setNaturalLanguageCode(String naturalLanguageCode) {
+        this.naturalLanguageCode = naturalLanguageCode;
+    }
+
+    public List<String> getPkgNames() {
+        return pkgNames;
+    }
+
+    public void setPkgNames(List<String> pkgNames) {
+        this.pkgNames = pkgNames;
+    }
+
+    public Integer getLimit() {
+        return limit;
+    }
+
+    public void setLimit(Integer limit) {
+        this.limit = limit;
+    }
+
+    public List<SupplierType> getSupplierTypes() {
+        return supplierTypes;
+    }
+
+    public void setSupplierTypes(List<SupplierType> supplierTypes) {
+        this.supplierTypes = supplierTypes;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        FeedSpecification that = (FeedSpecification) o;
+
+        if (!limit.equals(that.limit)) return false;
+ if (!naturalLanguageCode.equals(that.naturalLanguageCode)) return false; + if (pkgNames != null ? !pkgNames.equals(that.pkgNames) : that.pkgNames != null) return false;
+        if (!supplierTypes.equals(that.supplierTypes)) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = naturalLanguageCode.hashCode();
+ result = 31 * result + (pkgNames != null ? pkgNames.hashCode() : 0);
+        result = 31 * result + limit.hashCode();
+        result = 31 * result + supplierTypes.hashCode();
+        return result;
+    }
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/feed/model/SyndEntrySupplier.java Thu Aug 14 11:03:33 2014 UTC
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.feed.model;
+
+import com.sun.syndication.feed.synd.SyndEntry;
+
+import java.util.List;
+
+/**
+ * <p>Implementers of this interface are able to produce sync entry objects that are able to
+ * form part of an RSS/Atom feed.</p>
+ */
+
+public interface SyndEntrySupplier {
+
+    public final static String URI_PREFIX = "urn:hdsfeedentry:";
+
+    List<SyndEntry> generate(FeedSpecification specification);
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/CreatedPkgVersionSyndEntrySupplier.java Thu Aug 14 11:03:33 2014 UTC
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.pkg;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.hash.Hashing;
+import com.google.common.net.MediaType;
+import com.sun.syndication.feed.synd.SyndContent;
+import com.sun.syndication.feed.synd.SyndContentImpl;
+import com.sun.syndication.feed.synd.SyndEntry;
+import com.sun.syndication.feed.synd.SyndEntryImpl;
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.query.Ordering;
+import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.query.SortOrder;
+import org.haikuos.haikudepotserver.dataobjects.NaturalLanguage;
+import org.haikuos.haikudepotserver.dataobjects.Pkg;
+import org.haikuos.haikudepotserver.dataobjects.PkgVersion;
+import org.haikuos.haikudepotserver.dataobjects.PkgVersionLocalization;
+import org.haikuos.haikudepotserver.support.cayenne.ExpressionHelper;
+import org.haikuos.haikudepotserver.feed.model.FeedSpecification;
+import org.haikuos.haikudepotserver.feed.model.SyndEntrySupplier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.MessageSource;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * <p>This class produces RSS feed entries related to new pkg versions.</p>
+ */
+
+@Component
+public class CreatedPkgVersionSyndEntrySupplier implements SyndEntrySupplier {
+
+    @Resource
+    ServerRuntime serverRuntime;
+
+    @Value("${baseurl}")
+    String baseUrl;
+
+    @Resource
+    MessageSource messageSource;
+
+    @Override
+ public List<SyndEntry> generate(final FeedSpecification specification) {
+        Preconditions.checkNotNull(specification);
+
+ if(specification.getSupplierTypes().contains(FeedSpecification.SupplierType.CREATEDPKGVERSION)) {
+
+ if(null!=specification.getPkgNames() && specification.getPkgNames().isEmpty()) {
+                return Collections.emptyList();
+            }
+
+            List<Expression> expressions = Lists.newArrayList();
+
+            if(null!=specification.getPkgNames()) {
+                expressions.add(ExpressionFactory.inExp(
+ PkgVersion.PKG_PROPERTY + "." + Pkg.NAME_PROPERTY,
+                                specification.getPkgNames())
+                );
+            }
+
+            expressions.add(ExpressionFactory.matchExp(
+                            PkgVersion.ACTIVE_PROPERTY,
+                            Boolean.TRUE)
+            );
+
+            expressions.add(ExpressionFactory.matchExp(
+ PkgVersion.PKG_PROPERTY + "." + Pkg.ACTIVE_PROPERTY,
+                            Boolean.TRUE)
+            );
+
+            SelectQuery query = new SelectQuery(
+                    PkgVersion.class,
+                    ExpressionHelper.andAll(expressions));
+
+            query.addOrdering(new Ordering(
+                    PkgVersion.CREATE_TIMESTAMP_PROPERTY,
+                    SortOrder.DESCENDING));
+
+            query.setFetchLimit(specification.getLimit());
+
+ List<PkgVersion> pkgVersions = serverRuntime.getContext().performQuery(query);
+
+            return Lists.transform(
+                    pkgVersions,
+                    new Function<PkgVersion, SyndEntry>() {
+                        @Override
+                        public SyndEntry apply(PkgVersion input) {
+
+                            SyndEntry entry = new SyndEntryImpl();
+
+ entry.setPublishedDate(input.getCreateTimestamp()); + entry.setUpdatedDate(input.getModifyTimestamp());
+                            entry.setUri(URI_PREFIX +
+                                    Hashing.sha1().hashUnencodedChars(
+                                            String.format(
+                                                    "%s_::_%s_::_%s",
+ this.getClass().getCanonicalName(), + input.getPkg().getName(), + input.toVersionCoordinates().toString())).toString());
+
+                            entry.setLink(String.format(
+                                    "%s/#/pkg/%s/%s/%s/%s/%s/%d/%s",
+                                    baseUrl,
+                                    input.getPkg().getName(),
+                                    input.getMajor(),
+ null == input.getMinor() ? "" : input.getMinor(), + null == input.getMicro() ? "" : input.getMicro(), + null == input.getPreRelease() ? "" : input.getPreRelease(), + null == input.getRevision() ? "" : input.getRevision(),
+                                    input.getArchitecture().getCode()));
+
+                            entry.setTitle(messageSource.getMessage(
+                                    "feed.createdPkgVersion.atom.title",
+ new Object[] { input.toStringWithPkgAndArchitecture() }, + new Locale(specification.getNaturalLanguageCode())
+                            ));
+
+                            {
+ Optional<PkgVersionLocalization> pkgVersionLocalizationOptional = input.getPkgVersionLocalization(specification.getNaturalLanguageCode());
+
+ if(!pkgVersionLocalizationOptional.isPresent()) { + pkgVersionLocalizationOptional = input.getPkgVersionLocalization(NaturalLanguage.CODE_ENGLISH);
+                                }
+
+ SyndContent content = new SyndContentImpl(); + content.setType(MediaType.PLAIN_TEXT_UTF_8.type()); + content.setValue(pkgVersionLocalizationOptional.get().getSummary());
+                                entry.setDescription(content);
+                            }
+
+                            return entry;
+                        }
+                    }
+            );
+
+        }
+
+        return Collections.emptyList();
+    }
+
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/web/ErrorServlet.java Thu Aug 14 11:03:33 2014 UTC
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.support.web;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.html.HtmlEscapers;
+import com.google.common.net.MediaType;
+import org.haikuos.haikudepotserver.api1.support.Constants;
+import org.haikuos.haikudepotserver.dataobjects.NaturalLanguage;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * <p>This servlet gets hit whenever anything goes wrong in the application from a user perspective. It will + * say that a problem has arisen and off the user the opportunity to re-enter the application. It is as + * simple as possible to reduce the possibility of the error page failing as well.</p>
+ */
+
+public class ErrorServlet extends HttpServlet {
+
+    private final static String PARAM_JSONRPCERRORCODE = "jrpcerrorcd";
+
+    private final static Map<String,String> PREFIX = ImmutableMap.of(
+            NaturalLanguage.CODE_ENGLISH, "Oh darn!",
+            NaturalLanguage.CODE_GERMAN, "Oh mei!");
+
+    private final static Map<String,String> BODY_GENERAL = ImmutableMap.of(
+ NaturalLanguage.CODE_ENGLISH, "Something has gone wrong with your use of this web application.", + NaturalLanguage.CODE_GERMAN, "Etwas ist falsch gegangen mit Ihre Benutzung des Anwendungs.");
+
+ private final static Map<String,String> BODY_NOTFOUND = ImmutableMap.of( + NaturalLanguage.CODE_ENGLISH, "The requested resource was not able to be found.", + NaturalLanguage.CODE_GERMAN, "Die angefragte Ressource wurde nicht gefunden.");
+
+ private final static Map<String,String> BODY_AUTHORIZATIONFAILURE = ImmutableMap.of( + NaturalLanguage.CODE_ENGLISH, "Your authentication with the service is expired or you have reached a page that is not accessible with the level of your permissions.", + NaturalLanguage.CODE_GERMAN, "Die Berechtigungen für diesen Dienst sind abgelaufen, oder der Zugang zur angeforderten Seite erfordert zusätzliche Zugriffsrechte.");
+
+    private final static Map<String,String> ACTION = ImmutableMap.of(
+            NaturalLanguage.CODE_ENGLISH, "Start again",
+            NaturalLanguage.CODE_GERMAN, "Neue anfangen");
+
+    private byte[] pageGeneralBytes = null;
+
+ private Map<String,String> deriveBody(Integer jsonRpcErrorCode, Integer httpStatusCode) { + if (null != jsonRpcErrorCode && Constants.ERROR_CODE_AUTHORIZATIONFAILURE == jsonRpcErrorCode) {
+            return BODY_AUTHORIZATIONFAILURE;
+        }
+
+        if(null != httpStatusCode && 404 == httpStatusCode) {
+            return BODY_NOTFOUND;
+        }
+
+        return BODY_GENERAL;
+    }
+
+ private void messageLineAssembly(String naturalLanguageCode, StringBuilder out, Integer jsonRpcErrorCode, Integer httpStatusCode) {
+        HtmlEscapers.htmlEscaper();
+        out.append("<div class=\"error-message-container\">\n");
+        out.append("<div class=\"error-message\">\n");
+        out.append("<strong>");
+ out.append(HtmlEscapers.htmlEscaper().escape(PREFIX.get(naturalLanguageCode)));
+        out.append("</strong>");
+        out.append(" ");
+ out.append(HtmlEscapers.htmlEscaper().escape(deriveBody(jsonRpcErrorCode, httpStatusCode).get(naturalLanguageCode)));
+        out.append("</div>\n");
+        out.append("<div class=\"error-startagain\">");
+        out.append(" &#8594; ");
+        out.append("<a href=\"/\">");
+ out.append(HtmlEscapers.htmlEscaper().escape(ACTION.get(naturalLanguageCode)));
+        out.append("</a>");
+        out.append("</div>\n");
+        out.append("</div>\n");
+    }
+
+    /**
+ * <p>Assemble the page using code in order to reduce the chance of things going wrong loading resources and so
+     * on.</p>
+     */
+
+ private void pageAssembly(StringBuilder out, Integer jsonRpcErrorCode, Integer httpStatusCode) {
+
+        out.append("<html>\n");
+        out.append("<head>\n");
+ out.append("<link rel=\"icon\" type=\"image/png\" href=\"/img/haikudepot16.png\" sizes=\"16x16\">\n"); + out.append("<link rel=\"icon\" type=\"image/png\" href=\"/img/haikudepot32.png\" sizes=\"32x32\">\n"); + out.append("<link rel=\"icon\" type=\"image/png\" href=\"/img/haikudepot64.png\" sizes=\"64x64\">\n");
+        out.append("<title>HaikuDepotServer - Error</title>\n");
+        out.append("<style>\n");
+ out.append("body { background-color: #336698; position: relative; font-family: sans-serif; }\n");
+        out.append("h1 { text-align: center; }\n");
+ out.append("#error-container { color: white; width: 420px; height: 320px; margin:0 auto; margin-top: 72px; }\n"); + out.append("#error-container .error-message-container { margin-bottom: 20px; }\n"); + out.append("#error-container .error-startagain { text-align: right; }\n"); + out.append("#error-container .error-startagain > a { color: white; }\n");
+        out.append("#error-image { text-align: center; }\n");
+        out.append("</style>\n");
+        out.append("</head>\n");
+        out.append("<body>\n");
+        out.append("<div id=\"error-container\">\n");
+ out.append("<div id=\"error-image\"><img src=\"/img/haikudepot-error.svg\"></div>\n");
+        out.append("<h1>Haiku Depot Server</h1>\n");
+
+        for(String naturalLanguageCode : new String[] {
+                NaturalLanguage.CODE_ENGLISH,
+                NaturalLanguage.CODE_GERMAN }) {
+            messageLineAssembly(
+                    naturalLanguageCode,
+                    out,
+                    jsonRpcErrorCode,
+                    httpStatusCode);
+        }
+
+        out.append("</div>\n");
+        out.append("</body>\n");
+        out.append("</html>\n");
+    }
+
+    @Override
+    public void init(ServletConfig config) throws ServletException {
+ // get together the basic general page as a fallback that exists in memory so that in the worst
+        // case scenario, if memory is tight at least we can output this.
+
+        StringBuilder out = new StringBuilder();
+        pageAssembly(out, null, null);
+        pageGeneralBytes = out.toString().getBytes(Charsets.UTF_8);
+    }
+
+    @Override
+ protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+
+
+        try {
+            resp.setContentType(MediaType.HTML_UTF_8.toString());
+ String jsonRpcErrorCodeString = req.getParameter(PARAM_JSONRPCERRORCODE); + Integer jsonRpcErrorCode = Strings.isNullOrEmpty(jsonRpcErrorCodeString) ? null : Integer.parseInt(jsonRpcErrorCodeString);
+
+            byte pageBytes[] = pageGeneralBytes;
+
+            // special handling for JSON-RPC errors
+
+
+            if(null!=jsonRpcErrorCode || 404 == resp.getStatus()) {
+
+                try {
+                    StringBuilder out = new StringBuilder();
+                    pageAssembly(out, jsonRpcErrorCode, resp.getStatus());
+                    pageBytes = out.toString().getBytes(Charsets.UTF_8);
+                }
+                catch(Throwable th) {
+                    // swallow
+                }
+
+            }
+
+            resp.setContentLength(pageBytes.length);
+            resp.getOutputStream().write(pageBytes);
+            resp.getOutputStream().flush();
+
+        }
+        catch(Throwable th) {
+            // swallow
+        }
+
+    }
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/userrating/CreatedUserRatingSyndEntrySupplier.java Thu Aug 14 11:03:33 2014 UTC
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+package org.haikuos.haikudepotserver.userrating;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.hash.Hashing;
+import com.google.common.net.MediaType;
+import com.sun.syndication.feed.synd.SyndContentImpl;
+import com.sun.syndication.feed.synd.SyndEntry;
+import com.sun.syndication.feed.synd.SyndEntryImpl;
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.query.Ordering;
+import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.query.SortOrder;
+import org.haikuos.haikudepotserver.dataobjects.Pkg;
+import org.haikuos.haikudepotserver.dataobjects.PkgVersion;
+import org.haikuos.haikudepotserver.dataobjects.UserRating;
+import org.haikuos.haikudepotserver.support.cayenne.ExpressionHelper;
+import org.haikuos.haikudepotserver.feed.model.FeedSpecification;
+import org.haikuos.haikudepotserver.feed.model.SyndEntrySupplier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.MessageSource;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * <p>This class produces RSS feed entries related to user ratings.</p>
+ */
+
+@Component
+public class CreatedUserRatingSyndEntrySupplier implements SyndEntrySupplier {
+
+    private final static int CONTENT_LENGTH = 80;
+
+    private final char STAR_FILLED = '*'; // '\u2605';
+    private final char STAR_HOLLOW = '.'; // '\u2606';
+
+    @Resource
+    ServerRuntime serverRuntime;
+
+    @Value("${baseurl}")
+    String baseUrl;
+
+    @Resource
+    MessageSource messageSource;
+
+    /**
+ * <p>Produces a string containing a list of stars; either hollow or filled to indicate the user rating from
+     * zero to five.  The stars are made from unicode chars.</p>
+     */
+
+    private String buildRatingIndicator(int rating) {
+        StringBuilder buffer = new StringBuilder();
+        buildRatingIndicator(buffer, rating);
+        return buffer.toString();
+    }
+
+    /**
+ * <p>This is a recursive function to build the list of stars for the rating.</p>
+     */
+
+ private StringBuilder buildRatingIndicator(StringBuilder buffer, int rating) {
+        Preconditions.checkNotNull(buffer);
+        Preconditions.checkState(rating >= 0 && rating <= 5);
+
+        if(buffer.length() < (rating==5 ? 9 : rating*2)) {
+            buffer.append(STAR_FILLED);
+        }
+        else {
+            buffer.append(STAR_HOLLOW);
+        }
+
+        if(9 != buffer.length()) {
+            buffer.append(" ");
+        }
+        else {
+            return buffer;
+        }
+
+        return buildRatingIndicator(buffer,rating);
+    }
+
+    @Override
+ public List<SyndEntry> generate(final FeedSpecification specification) {
+        Preconditions.checkNotNull(specification);
+
+ if(specification.getSupplierTypes().contains(FeedSpecification.SupplierType.CREATEDUSERRATING)) {
+
+ if(null!=specification.getPkgNames() && specification.getPkgNames().isEmpty()) {
+                return Collections.emptyList();
+            }
+
+            List<Expression> expressions = Lists.newArrayList();
+
+            if(null!=specification.getPkgNames()) {
+                expressions.add(ExpressionFactory.inExp(
+ UserRating.PKG_VERSION_PROPERTY + "." + PkgVersion.PKG_PROPERTY + "." + Pkg.NAME_PROPERTY,
+                                specification.getPkgNames())
+                );
+            }
+
+            expressions.add(ExpressionFactory.matchExp(
+                            UserRating.ACTIVE_PROPERTY,
+                            Boolean.TRUE)
+            );
+
+            expressions.add(ExpressionFactory.matchExp(
+ UserRating.PKG_VERSION_PROPERTY + "." + PkgVersion.ACTIVE_PROPERTY,
+                            Boolean.TRUE)
+            );
+
+            expressions.add(ExpressionFactory.matchExp(
+ UserRating.PKG_VERSION_PROPERTY + "." + PkgVersion.PKG_PROPERTY + "." + Pkg.ACTIVE_PROPERTY,
+                            Boolean.TRUE)
+            );
+
+            SelectQuery query = new SelectQuery(
+                    UserRating.class,
+                    ExpressionHelper.andAll(expressions));
+
+            query.addOrdering(new Ordering(
+                    UserRating.CREATE_TIMESTAMP_PROPERTY,
+                    SortOrder.DESCENDING));
+
+            query.setFetchLimit(specification.getLimit());
+
+ List<UserRating> userRatings = serverRuntime.getContext().performQuery(query);
+
+            return Lists.transform(
+                    userRatings,
+                    new Function<UserRating, SyndEntry>() {
+                        @Override
+                        public SyndEntry apply(UserRating input) {
+
+                            SyndEntry entry = new SyndEntryImpl();
+ entry.setPublishedDate(input.getCreateTimestamp()); + entry.setUpdatedDate(input.getModifyTimestamp());
+                            entry.setAuthor(input.getUser().getNickname());
+                            entry.setUri(URI_PREFIX +
+                                    Hashing.sha1().hashUnencodedChars(
+                                            String.format(
+                                                    "%s_::_%s_::_%s_::_%s",
+ this.getClass().getCanonicalName(), + input.getPkgVersion().getPkg().getName(), + input.getPkgVersion().toVersionCoordinates().toString(), + input.getUser().getNickname())).toString());
+                            entry.setLink(String.format(
+                                    "%s/#/userrating/%s",
+                                    baseUrl,
+                                    input.getCode()));
+
+                            entry.setTitle(messageSource.getMessage(
+                                    "feed.createdUserRating.atom.title",
+                                    new Object[]{
+ input.getPkgVersion().toStringWithPkgAndArchitecture(),
+                                            input.getUser().getNickname()
+                                    },
+ new Locale(specification.getNaturalLanguageCode())
+                            ));
+
+                            String contentString = input.getComment();
+
+ if(null!=contentString && contentString.length() > CONTENT_LENGTH) { + contentString = contentString.substring(0,CONTENT_LENGTH) + "...";
+                            }
+
+ // if there is a rating then express this as a string using unicode
+                            // characters.
+
+                            if(null!=input.getRating()) {
+ contentString = buildRatingIndicator(input.getRating()) + + (Strings.isNullOrEmpty(contentString) ? "" : " -- " + contentString);
+                            }
+
+ SyndContentImpl content = new SyndContentImpl(); + content.setType(MediaType.PLAIN_TEXT_UTF_8.type());
+                            content.setValue(contentString);
+                            entry.setDescription(content);
+
+                            return entry;
+                        }
+                    }
+            );
+
+        }
+
+        return Collections.emptyList();
+    }
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/css/pkgfeedbuilder.css Thu Aug 14 11:03:33 2014 UTC
@@ -0,0 +1,21 @@
+.pkg-feed-builder .feed-url-container {
+    font-family: "Courier New", Courier, mono;
+    padding: 4px 8px;
+    border: 1px solid black;
+    background-color: #EEE;
+}
+
+.pkg-feed-builder .pkg-lozenge-container {
+    padding-top: 3px;
+    padding-bottom: 3px;
+}
+
+.pkg-feed-builder .pkg-lozenge-container .pkg-lozenge {
+    background-color: #EEE;
+    border-radius: 4px;
+    padding: 2px 6px;
+    display: inline-block;
+    margin-left: 2px;
+    margin-right: 2px;
+    border: 1px solid lightgray;
+}
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/img/feed.svg Thu Aug 14 11:03:33 2014 UTC
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns="http://www.w3.org/2000/svg";
+   version="1.1"
+   width="16"
+   height="16">
+ <rect stroke="none" fill="#f08934" x="0" y="0" width="16" height="16" rx="3" ry="3"/>
+  <circle stroke="none" fill="white" cx="4" cy="12" r="2"/>
+ <path fill="none" stroke="white" stroke-width="2" d="M3 7 a 6 6 90 0 1 6 6"/> + <path fill="none" stroke="white" stroke-width="2" d="M3 3 a 10 10 90 0 1 10 10"/>
+</svg>
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/pkgfeedbuilder.html Thu Aug 14 11:03:33 2014 UTC
@@ -0,0 +1,103 @@
+<breadcrumbs items="breadcrumbItems"></breadcrumbs>
+
+<div class="content-container pkg-feed-builder">
+
+    <div ng-show="feedUrl">
+
+        <div class="feed-url-container">{{feedUrl}}</div>
+
+        <h2><message key="gen.actions.title"></message></h2>
+
+        <ul>
+            <li>
+                <a href="" ng-click="goEdit()">
+ <message key="pkgFeedBuilder.editAction.title"></message>
+                </a>
+            </li>
+            <li>
+                <a href="{{feedUrl}}" target="_blank">
+ <message key="pkgFeedBuilder.openFeedAction.title"></message>
+                </a>
+            </li>
+        </ul>
+    </div>
+    <div ng-show="!feedUrl">
+
+        <div class="form-info-container">
+            <message key="pkgFeedBuilder.info"></message>
+        </div>
+
+        <form name="feedForm" novalidate="novalidate">
+
+ <label for="limit"><message key="pkgFeedBuilder.limit.title"></message></label> + <div class="form-control-group" ng-class="deriveFormControlsContainerClasses('limit')">
+                <select
+                        name="limit"
+                        id="limit"
+                        required="true"
+                        ng-model="feedSettings.limit"
+                        ng-options="aLimit for aLimit in limits"></select>
+            </div>
+
+ <label><message key="pkgFeedBuilder.pkgs.title"></message></label>
+            <div class="form-control-group">
+
+                <!-- START; PKG CHOOSER -->
+
+                <div ng-show="0==feedSettings.pkgs.length">
+ <em><message key="pkgFeedBuilder.pkgs.all"></message></em>
+                </div>
+ <div ng-show="feedSettings.pkgs.length > 0" class="pkg-lozenge-container"> + <div class="pkg-lozenge" ng-repeat="pkg in feedSettings.pkgs">
+                        <pkg-icon pkg="pkg" size="16"></pkg-icon>
+                        <pkg-label pkg="pkg"></pkg-label>
+ <img src="/img/rowdelete.svg" ng-click="goRemovePkg(pkg)">
+                    </div>
+                </div>
+
+                <div>
+                    <input
+                            type="text"
+                            placeholder="{{pkgNamePlaceholder}}"
+                            name="pkgChooserName"
+                            autocomplete="off"
+                            size="12"
+                            ng-model="pkgChooserName"
+                            ng-pattern="{{pkgNamePattern}}">
+                    <button
+ ng-disabled="!pkgChooserName.length || feedForm.pkgChooserName.$invalid"
+                            ng-click="goAddPkg()"
+ type="submit"><message key="pkgFeedBuilder.pkgs.addAction.title"></message></button>
+                    <error-messages
+                            key-prefix="pkgFeedBuilder.pkgChooserName"
+ error="feedForm.pkgChooserName.$error"></error-messages>
+                </div>
+
+                <!-- END; PKG CHOOSER -->
+
+            </div>
+
+ <label><message key="pkgFeedBuilder.supplierTypes.title"></message></label>
+            <div class="form-control-group">
+ <div ng-repeat="supplierType in feedSettings.supplierTypes">
+                    <input
+                            type="checkbox"
+ ng-model="supplierType.selected"> {{supplierType.title}}
+                </div>
+            </div>
+
+            <div class="form-action-container">
+                <button
+                        ng-disabled="addRuleForm.$invalid"
+                        ng-click="goBuild()"
+                        type="submit"
+ class="main-action"><message key="pkgFeedBuilder.action.title"></message></button>
+            </div>
+
+        </form>
+    </div>
+</div>
+
+<div class="footer"></div>
+<spinner spin="shouldSpin()"></spinner>
+
=======================================
--- /dev/null
+++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/pkgfeedbuildercontroller.js Thu Aug 14 11:03:33 2014 UTC
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2014, Andrew Lindesay
+ * Distributed under the terms of the MIT License.
+ */
+
+/**
+ * <p>This controller allows the user to choose aspects of a package feed and to be able to
+ * configure a URL which can be used with a feed reader.</p>
+ */
+
+angular.module('haikudepotserver').controller(
+    'PkgFeedBuilderController',
+    [
+        '$scope','$log','$location',
+        'jsonRpc','constants','messageSource','referenceData',
+        'breadcrumbs','breadcrumbFactory','errorHandling','userState',
+        function(
+            $scope,$log,$location,
+            jsonRpc,constants,messageSource,referenceData,
+            breadcrumbs,breadcrumbFactory,errorHandling,userState) {
+
+            breadcrumbs.mergeCompleteStack([
+                breadcrumbFactory.createHome(),
+ breadcrumbFactory.applyCurrentLocation(breadcrumbFactory.createPkgFeedBuilder())
+            ]);
+
+            $scope.pkgChooserName = '';
+            $scope.pkgNamePattern = '' + constants.PATTERN_PKG_NAME;
+            $scope.pkgNamePlaceholder = undefined;
+            $scope.feedUrl = undefined;
+            $scope.limits = [5,10,25,50,75,100];
+            $scope.feedSettings = {
+                pkgs: [],
+                limit: 25,
+                supplierTypes: undefined
+            };
+
+            var amBuilding = false;
+
+            $scope.shouldSpin = function() {
+                return !$scope.feedSettings.supplierTypes || amBuilding;
+            };
+
+            messageSource.get(
+                userState.naturalLanguageCode(),
+                'pkgFeedBuilder.pkgChooserName.placeholder')
+                .then(
+                function(text) { $scope.pkgNamePlaceholder = text; }
+            );
+
+            function refreshFeedSupplierTypes() {
+                referenceData.feedSupplierTypes().then(
+                    function(items) {
+
+ $scope.feedSettings.supplierTypes = _.map(items, function(item) {
+                            return {
+                                code : item.code,
+                                title : '...',
+                                selected : true
+                            };
+                        });
+
+                        _.each(
+                            $scope.feedSettings.supplierTypes,
+                            function(item) {
+                                messageSource.get(
+                                    userState.naturalLanguageCode(),
+                                        
'feed.source.'+item.code.toLowerCase()+'.title'
+                                ).then(
+                                    function(title) {
+                                        item.title = title;
+                                    },
+                                    function() {
+                                        item.title = '???';
+                                    }
+                                );
+                            }
+                        );
+
+                    },
+                    function() {
+                        errorHandling.navigateToError();
+                    }
+                )
+            }
+
+            refreshFeedSupplierTypes();
+
+            /**
+ * <p>This function will build the feed URL; it calls back to the server to do this in order to + * avoid coding too much of the URL building logic in the page.</p>
+             */
+
+            function build() {
+
+                amBuilding = true;
+
+                var pkgNames = null;
+                var supplierTypeCodes = _.pluck(
+ _.where($scope.feedSettings.supplierTypes,{selected:true}),
+                    'code'
+                );
+
+                if($scope.feedSettings.pkgs.length) {
+                    pkgNames = _.pluck($scope.feedSettings.pkgs,'name');
+                }
+
+                jsonRpc.call(
+                    constants.ENDPOINT_API_V1_MISCELLANEOUS,
+                    'generateFeedUrl',
+                    [{
+ naturalLanguageCode : userState.naturalLanguageCode(),
+                        pkgNames : pkgNames,
+                        limit : $scope.feedSettings.limit,
+                        supplierTypes : supplierTypeCodes
+                    }]
+                ).then(
+                    function(result) {
+                        $scope.feedUrl = result.url;
+                        amBuilding = false;
+                    },
+                    function(err) {
+                        $log.error('unable to generate feed url');
+                        errorHandling.handleJsonRpcError(err);
+                    }
+                );
+
+            }
+
+            /**
+ * <p>This function will add a package to the list of specific packages using the
+             * supplied name to identify the package.</p>
+             */
+
+            function initialAddPkgs() {
+
+                var initialPkgNamesString = $location.search()['pkgNames'];
+
+                if(initialPkgNamesString && initialPkgNamesString.length) {
+
+ // split on hyphen because it is not allowed in a package name.
+
+ _.each(initialPkgNamesString.split('-'),function(initialPkgNames) {
+                        jsonRpc.call(
+                            constants.ENDPOINT_API_V1_PKG,
+                            'getPkg',
+                            [
+                                {
+ naturalLanguageCode: userState.naturalLanguageCode(),
+                                    name: initialPkgNames,
+                                    versionType: 'NONE'
+                                }
+                            ]
+                        ).then(
+                            function (pkg) {
+                                $scope.feedSettings.pkgs.push(pkg);
+                            },
+                            function () {
+ $log.warn('unable to initially populate the packages');
+                            }
+                        );
+                    });
+                }
+            }
+
+            initialAddPkgs();
+
+            // ------------------
+            // ACTIONS
+
+            $scope.goBuild = function() {
+                build();
+            }
+
+            $scope.goEdit = function() {
+                $scope.feedUrl = undefined;
+            }
+
+            $scope.goRemovePkg = function(pkg) {
+ $scope.feedSettings.pkgs = _.without($scope.feedSettings.pkgs,pkg);
+            }
+
+            /**
+ * <P>This is made invalid as part of searching for the package. If the package name is
+             * changed then the invalidity no longer holds.</P>
+             */
+
+            $scope.$watch('pkgChooserName', function() {
+ $scope.feedForm.pkgChooserName.$setValidity('notfound',true);
+            })
+
+            $scope.goAddPkg = function() {
+
+                jsonRpc.call(
+                    constants.ENDPOINT_API_V1_PKG,
+                    'getPkg',
+                    [{
+ naturalLanguageCode : userState.naturalLanguageCode(),
+                        name : $scope.pkgChooserName,
+                        versionType : 'NONE'
+                    }]
+                ).then(
+                    function(pkg) {
+                        $scope.pkgChooserName = '';
+                        $scope.feedSettings.pkgs.push(pkg);
+                    },
+                    function(err) {
+
+                        switch(err.code) {
+
+                            case jsonRpc.errorCodes.OBJECTNOTFOUND:
+ $scope.feedForm.pkgChooserName.$setValidity('notfound',false);
+                                break;
+
+                            default:
+ $log.error('unable to get the pkg for name; ' + $scope.pkgChooserName);
+                                errorHandling.handleJsonRpcError(err);
+                                break;
+
+                        }
+
+                    }
+                );
+
+            }
+
+        }
+    ]
+);
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/support/web/ErrorFilter.java Tue Jul 1 10:10:47 2014 UTC
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright 2014, Andrew Lindesay
- * Distributed under the terms of the MIT License.
- */
-
-package org.haikuos.haikudepotserver.support.web;
-
-import com.google.common.base.Charsets;
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.html.HtmlEscapers;
-import com.google.common.net.MediaType;
-import org.haikuos.haikudepotserver.api1.support.Constants;
-import org.haikuos.haikudepotserver.dataobjects.NaturalLanguage;
-
-import javax.servlet.*;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
-import java.util.Map;
-
-/**
- * <p>This filter gets hit whenever anything goes wrong in the application from a user perspective. It will - * say that a problem has arisen and off the user the opportunity to re-enter the application. It is as - * simple as possible to reduce the possibility of the error page failing as well.</p>
- */
-
-public class ErrorFilter implements Filter {
-
-    private final static String PARAM_JSONRPCERRORCODE = "jrpcerrorcd";
-
-    private final static Map<String,String> PREFIX = ImmutableMap.of(
-            NaturalLanguage.CODE_ENGLISH, "Oh darn!",
-            NaturalLanguage.CODE_GERMAN, "Oh mei!");
-
-    private final static Map<String,String> BODY_GENERAL = ImmutableMap.of(
- NaturalLanguage.CODE_ENGLISH, "Something has gone wrong with your use of this web application.", - NaturalLanguage.CODE_GERMAN, "Etwas ist falsch gegangen mit Ihre Benutzung des Anwendungs.");
-
- private final static Map<String,String> BODY_AUTHORIZATIONFAILURE = ImmutableMap.of( - NaturalLanguage.CODE_ENGLISH, "Your authentication with the service is expired or you have reached a page that is not accessible with the level of your permissions.", - NaturalLanguage.CODE_GERMAN, "Die Berechtigungen für diesen Dienst sind abgelaufen, oder der Zugang zur angeforderten Seite erfordert zusätzliche Zugriffsrechte.");
-
-    private final static Map<String,String> ACTION = ImmutableMap.of(
-            NaturalLanguage.CODE_ENGLISH, "Start again",
-            NaturalLanguage.CODE_GERMAN, "Neue anfangen");
-
-    private byte[] pageGeneralBytes = null;
-
-    private Map<String,String> deriveBody(Integer jsonRpcErrorCode) {
- if (null != jsonRpcErrorCode && Constants.ERROR_CODE_AUTHORIZATIONFAILURE == jsonRpcErrorCode) {
-            return BODY_AUTHORIZATIONFAILURE;
-        }
-        return BODY_GENERAL;
-    }
-
- private void messageLineAssembly(String naturalLanguageCode, StringBuilder out, Integer jsonRpcErrorCode) {
-        HtmlEscapers.htmlEscaper();
-        out.append("<div class=\"error-message-container\">\n");
-        out.append("<div class=\"error-message\">\n");
-        out.append("<strong>");
- out.append(HtmlEscapers.htmlEscaper().escape(PREFIX.get(naturalLanguageCode)));
-        out.append("</strong>");
-        out.append(" ");
- out.append(HtmlEscapers.htmlEscaper().escape(deriveBody(jsonRpcErrorCode).get(naturalLanguageCode)));
-        out.append("</div>\n");
-        out.append("<div class=\"error-startagain\">");
-        out.append(" &#8594; ");
-        out.append("<a href=\"/\">");
- out.append(HtmlEscapers.htmlEscaper().escape(ACTION.get(naturalLanguageCode)));
-        out.append("</a>");
-        out.append("</div>\n");
-        out.append("</div>\n");
-    }
-
-    /**
- * <p>Assemble the page using code in order to reduce the chance of things going wrong loading resources and so
-     * on.</p>
-     */
-
- private void pageAssembly(StringBuilder out, Integer jsonRpcErrorCode) {
-
-        out.append("<html>\n");
-        out.append("<head>\n");
- out.append("<link rel=\"icon\" type=\"image/png\" href=\"/img/haikudepot16.png\" sizes=\"16x16\">\n"); - out.append("<link rel=\"icon\" type=\"image/png\" href=\"/img/haikudepot32.png\" sizes=\"32x32\">\n"); - out.append("<link rel=\"icon\" type=\"image/png\" href=\"/img/haikudepot64.png\" sizes=\"64x64\">\n");
-        out.append("<title>HaikuDepotServer - Error</title>\n");
-        out.append("<style>\n");
- out.append("body { background-color: #336698; position: relative; font-family: sans-serif; }\n");
-        out.append("h1 { text-align: center; }\n");
- out.append("#error-container { color: white; width: 420px; height: 320px; margin:0 auto; margin-top: 72px; }\n"); - out.append("#error-container .error-message-container { margin-bottom: 20px; }\n"); - out.append("#error-container .error-startagain { text-align: right; }\n"); - out.append("#error-container .error-startagain > a { color: white; }\n");
-        out.append("#error-image { text-align: center; }\n");
-        out.append("</style>\n");
-        out.append("</head>\n");
-        out.append("<body>\n");
-        out.append("<div id=\"error-container\">\n");
- out.append("<div id=\"error-image\"><img src=\"/img/haikudepot-error.svg\"></div>\n");
-        out.append("<h1>Haiku Depot Server</h1>\n");
-
-        for(String naturalLanguageCode : new String[] {
-                NaturalLanguage.CODE_ENGLISH,
-                NaturalLanguage.CODE_GERMAN }) {
- messageLineAssembly(naturalLanguageCode, out, jsonRpcErrorCode);
-        }
-
-        out.append("</div>\n");
-        out.append("</body>\n");
-        out.append("</html>\n");
-    }
-
-    @Override
-    public void init(FilterConfig filterConfig) throws ServletException {
-
- // get together the basic general page as a fallback that exists in memory so that in the worst
-        // case scenario, if memory is tight at least we can output this.
-
-        StringBuilder out = new StringBuilder();
-        pageAssembly(out, null);
-        pageGeneralBytes = out.toString().getBytes(Charsets.UTF_8);
-    }
-
-    @Override
-    public void doFilter(
-            ServletRequest request,
-            ServletResponse response,
-            FilterChain chain) throws IOException, ServletException {
-
-        try {
-            HttpServletRequest httpRequest = (HttpServletRequest) request;
- HttpServletResponse httpResponse = (HttpServletResponse) response;
-            httpResponse.setContentType(MediaType.HTML_UTF_8.toString());
- String jsonRpcErrorCodeString = httpRequest.getParameter(PARAM_JSONRPCERRORCODE);
-
-            byte pageBytes[] = pageGeneralBytes;
-
-            if(!Strings.isNullOrEmpty(jsonRpcErrorCodeString)) {
-
-                try {
- Integer jsonRpcErrorCode = Integer.parseInt(jsonRpcErrorCodeString);
-                    StringBuilder out = new StringBuilder();
-                    pageAssembly(out, jsonRpcErrorCode);
-                    pageBytes = out.toString().getBytes(Charsets.UTF_8);
-                }
-                catch(Throwable th) {
-                    pageBytes = pageGeneralBytes;
-                }
-
-            }
-
-            httpResponse.setContentLength(pageBytes.length);
-            httpResponse.getOutputStream().write(pageBytes);
-            httpResponse.getOutputStream().flush();
-
-        }
-        catch(Throwable th) {
-            // eat it.
-        }
-
-    }
-
-    @Override
-    public void destroy() {
-        pageGeneralBytes = null;
-    }
-
-}
=======================================
--- /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApi.java Wed Aug 6 09:46:44 2014 UTC +++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApi.java Thu Aug 14 11:03:33 2014 UTC
@@ -66,4 +66,12 @@

GetAllProminencesResult getAllProminences(GetAllProminencesRequest request);

+    /**
+ * <p>This method will return a feed URL based on the supplied information in the request. If + * any of the elements supplied in the request do not exist then this method will throw an + * instance of {@link org.haikuos.haikudepotserver.api1.support.ObjectNotFoundException}.</p>
+     */
+
+ GenerateFeedUrlResult generateFeedUrl(GenerateFeedUrlRequest request) throws ObjectNotFoundException;
+
 }
=======================================
--- /haikudepotserver-docs/src/main/latex/docs/part-config.tex Thu Jul 24 11:35:00 2014 UTC +++ /haikudepotserver-docs/src/main/latex/docs/part-config.tex Thu Aug 14 11:03:33 2014 UTC
@@ -64,11 +64,17 @@

This configuration relates to the \href{https://jawr.java.net/}{JAWR} debug setting. It will default to ``false'.

-\subsubsection{\tt passwordreset.baseurl}
+\subsubsection{\tt baseurl}

-This is the base URL to which password-reset requests will be directed. The URL base is included in the email body to users who have requested a password-reset. This has to be configured because the application itself does not know the path by which the HTTP request had reached it. In the case of a development environment, this base URL might be something like; +This is the base URL used to formulate URLs to be sent out that can be used to return back to the system.

-\framebox{http://localhost:8080/passwordreset/}
+For example; this might be used to create the URL used to manage the password reset process. The URL base is included in the email body to users who have requested a password-reset. This has to be configured because the application itself does not know the path by which the HTTP request had reached it.
+
+In the case of a development environment, this base URL might be something like;
+
+\framebox{http://localhost:8080}
+
+Note that this value should have no trailing slash.

 \subsection{Token Bearer Authentication}

=======================================
--- /haikudepotserver-docs/src/main/latex/docs/part-localization.tex Mon Aug 4 00:20:12 2014 UTC +++ /haikudepotserver-docs/src/main/latex/docs/part-localization.tex Thu Aug 14 11:03:33 2014 UTC
@@ -47,7 +47,7 @@

 \subsection{Error Pages}

-The ``error page'' is a page that renders a message to indicate that, for some reason, the user's usage of the application cannot continue. The localization of this page is in-code logic because this approach yields a low probability that the rendering of the error page will result in further error. The text for this page can be found in the file {\it ErrorFilter.java}. +The ``error page'' is a page that renders a message to indicate that, for some reason, the user's usage of the application cannot continue. The localization of this page is in-code logic because this approach yields a low probability that the rendering of the error page will result in further error. The text for this page can be found in the file {\it ErrorServlet.java}.

 \subsection{Unsupported}

=======================================
--- /haikudepotserver-parent/pom.xml    Sun Aug  3 11:11:48 2014 UTC
+++ /haikudepotserver-parent/pom.xml    Thu Aug 14 11:03:33 2014 UTC
@@ -25,7 +25,7 @@
 within the jars themselves.
 -->

-        <web-angularjs.versionbase>1.2.20</web-angularjs.versionbase>
+        <web-angularjs.versionbase>1.2.21</web-angularjs.versionbase>
         <web-angularjs.versionextension></web-angularjs.versionextension>

         <web-underscorejs.versionbase>1.6.0</web-underscorejs.versionbase>
@@ -132,6 +132,12 @@
                 <artifactId>jawr</artifactId>
                 <version>3.3.3</version>
             </dependency>
+            <!-- rss -->
+            <dependency>
+                <groupId>rome</groupId>
+                <artifactId>rome</artifactId>
+                <version>1.0</version>
+            </dependency>

             <!-- DATABASE / PERSISTENCE -->
             <dependency>
=======================================
--- /haikudepotserver-rpm/src/main/etc/config__config.properties Sat Jul 12 08:36:41 2014 UTC +++ /haikudepotserver-rpm/src/main/etc/config__config.properties Thu Aug 14 11:03:33 2014 UTC
@@ -40,6 +40,12 @@

 #jawr.debug.on=false

+# This URL provides a base URL that the application can then add to when
+# it formulates URLs that are to be used outside of the application; for
+# example, URLs in ATOM feeds etc...
+
+baseurl=https://doesnotexist.haiku-os.org:8080/
+
 # -------------------------------------------
 # web security

@@ -69,3 +75,14 @@
 # commented out to force the value to be considered

 #authentication.jws.issuer=
+
+# -------------------------------------------
+# email-related
+
+smtp.host=localhost
+#smtp.port=2525
+#smtp.username=
+#smtp.password=
+#smtp.auth=false
+#smtp.starttls=false
+email.from=noreply@xxxxxxxxxxxx
=======================================
--- /haikudepotserver-webapp/pom.xml    Sun Aug  3 11:11:48 2014 UTC
+++ /haikudepotserver-webapp/pom.xml    Thu Aug 14 11:03:33 2014 UTC
@@ -88,6 +88,10 @@
             <groupId>net.jawr</groupId>
             <artifactId>jawr</artifactId>
         </dependency>
+        <dependency>
+            <groupId>rome</groupId>
+            <artifactId>rome</artifactId>
+        </dependency>

         <!-- DATABASE / PERSISTENCE -->
         <dependency>
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApiImpl.java Wed Aug 6 09:46:44 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/MiscellaneousApiImpl.java Thu Aug 14 11:03:33 2014 UTC
@@ -14,6 +14,8 @@
 import org.haikuos.haikudepotserver.api1.model.miscellaneous.*;
 import org.haikuos.haikudepotserver.api1.support.ObjectNotFoundException;
 import org.haikuos.haikudepotserver.dataobjects.*;
+import org.haikuos.haikudepotserver.feed.FeedOrchestrationService;
+import org.haikuos.haikudepotserver.feed.model.FeedSpecification;
 import org.haikuos.haikudepotserver.support.Closeables;
 import org.haikuos.haikudepotserver.support.RuntimeInformationService;
 import org.slf4j.Logger;
@@ -25,6 +27,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.util.List;
 import java.util.Map;
 import java.util.Properties;

@@ -41,6 +44,9 @@
     @Resource
     RuntimeInformationService runtimeInformationService;

+    @Resource
+    FeedOrchestrationService feedOrchestrationService;
+
     @Override
public GetAllPkgCategoriesResult getAllPkgCategories(GetAllPkgCategoriesRequest getAllPkgCategoriesRequest) {
         Preconditions.checkNotNull(getAllPkgCategoriesRequest);
@@ -239,5 +245,50 @@
                 )
         );
     }
+
+    @Override
+ public GenerateFeedUrlResult generateFeedUrl(final GenerateFeedUrlRequest request) throws ObjectNotFoundException {
+        Preconditions.checkNotNull(request);
+
+        final ObjectContext context = serverRuntime.getContext();
+        FeedSpecification specification = new FeedSpecification();
+        specification.setLimit(request.limit);
+
+        if(null!=request.supplierTypes) {
+            specification.setSupplierTypes(Lists.transform(
+                    request.supplierTypes,
+ new Function<GenerateFeedUrlRequest.SupplierType, FeedSpecification.SupplierType>() {
+                        @Override
+ public FeedSpecification.SupplierType apply(GenerateFeedUrlRequest.SupplierType input) { + return FeedSpecification.SupplierType.valueOf(input.name());
+                        }
+                    }
+            ));
+        }
+
+        if(null!=request.naturalLanguageCode) {
+ specification.setNaturalLanguageCode(getNaturalLanguage(context, request.naturalLanguageCode).getCode());
+        }
+
+        if(null!=request.pkgNames) {
+            List<String> checkedPkgNames = Lists.newArrayList();
+
+            for (String pkgName : request.pkgNames) {
+ Optional<Pkg> pkgOptional = Pkg.getByName(context, pkgName);
+
+                if (!pkgOptional.isPresent()) {
+ throw new ObjectNotFoundException(Pkg.class.getSimpleName(), pkgName);
+                }
+
+                checkedPkgNames.add(pkgOptional.get().getName());
+            }
+
+            specification.setPkgNames(checkedPkgNames);
+        }
+
+        GenerateFeedUrlResult result = new GenerateFeedUrlResult();
+        result.url = feedOrchestrationService.generateUrl(specification);
+        return result;
+    }

 }
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/PkgVersion.java Fri Aug 8 09:53:48 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/dataobjects/PkgVersion.java Thu Aug 14 11:03:33 2014 UTC
@@ -217,6 +217,10 @@
                 getPreRelease(),
                 getRevision());
     }
+
+    public String toStringWithPkgAndArchitecture() {
+ return getPkg().getName() + " - " + toString() + " - " + getArchitecture().getCode();
+    }

     @Override
     public String toString() {
=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/passwordreset/PasswordResetOrchestrationService.java Sun Aug 3 11:11:48 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/passwordreset/PasswordResetOrchestrationService.java Thu Aug 14 11:03:33 2014 UTC
@@ -60,7 +60,7 @@
     @Value("${passwordreset.ttlhours:1}")
     Integer timeToLiveHours;

-    @Value("${passwordreset.baseurl}")
+    @Value("${baseurl}")
     String baseUrl;

     @Value("${email.from}")
@@ -106,7 +106,7 @@
         userPasswordResetToken.setCreateTimestamp(new Date());

         PasswordResetMail mailModel = new PasswordResetMail();
-        mailModel.setPasswordResetBaseUrl(baseUrl);
+        mailModel.setPasswordResetBaseUrl(baseUrl + "/passwordreset/");
         mailModel.setUserNickname(user.getNickname());
mailModel.setUserPasswordResetTokenCode(userPasswordResetToken.getCode());

=======================================
--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/PkgOrchestrationService.java Fri Aug 8 09:53:48 2014 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/pkg/PkgOrchestrationService.java Thu Aug 14 11:03:33 2014 UTC
@@ -5,7 +5,10 @@

 package org.haikuos.haikudepotserver.pkg;

-import com.google.common.base.*;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
@@ -18,9 +21,6 @@
 import org.apache.cayenne.query.PrefetchTreeNode;
 import org.apache.cayenne.query.SelectQuery;
 import org.haikuos.haikudepotserver.dataobjects.*;
-import org.haikuos.haikudepotserver.dataobjects.Pkg;
-import org.haikuos.haikudepotserver.dataobjects.PkgUrlType;
-import org.haikuos.haikudepotserver.dataobjects.PkgVersion;
 import org.haikuos.haikudepotserver.pkg.model.BadPkgIconException;
 import org.haikuos.haikudepotserver.pkg.model.BadPkgScreenshotException;
 import org.haikuos.haikudepotserver.pkg.model.PkgSearchSpecification;
@@ -30,7 +30,6 @@
 import org.haikuos.haikudepotserver.support.VersionCoordinatesComparator;
 import org.haikuos.haikudepotserver.support.cayenne.ExpressionHelper;
 import org.haikuos.haikudepotserver.support.cayenne.LikeHelper;
-import org.haikuos.pkg.model.*;
 import org.imgscalr.Scalr;
 import org.joda.time.DateTime;
 import org.slf4j.Logger;
@@ -861,7 +860,7 @@
             else {
                 if(0==c) {
                     LOGGER.debug(
- "imported a package version {} of {} which is older or the same as the existing {}", + "imported a package version {} of {} which is the same as the existing {}",
                             persistedPkgVersionCoords,
                             persistedPkgVersion.getPkg().getName(),
                             persistedLatestExistingPkgVersionCoords);
=======================================
--- /haikudepotserver-webapp/src/main/resources/messages.properties Wed Aug 6 09:46:44 2014 UTC +++ /haikudepotserver-webapp/src/main/resources/messages.properties Thu Aug 14 11:03:33 2014 UTC
@@ -41,6 +41,7 @@
 breadcrumb.completePasswordReset.title=Password Reset
 breadcrumb.listAuthorizationPkgRules.title=Package Authorization Rules
 breadcrumb.addAuthorizationPkgRule.title=Add Rule
+breadcrumb.pkgFeedBuilder.title=Build Feed URL

 about.info.title=Information
 about.info.version=Version {0}
@@ -293,7 +294,7 @@
 addAuthorizationPkgRule.userNickname.title=User Nickname
addAuthorizationPkgRule.userNickname.required=A new rule has to be associated with a registered user. addAuthorizationPkgRule.userNickname.pattern=The user nickname is malformed. -addAuthorizationPkgRule.userNickname.notfound=There's no user registered with the supplied nickname. +addAuthorizationPkgRule.userNickname.notfound=There is no user registered with the supplied nickname.
 addAuthorizationPkgRule.permission.title=Permission
 addAuthorizationPkgRule.authorizationTargetScopeType.apkg.title=A Package
addAuthorizationPkgRule.authorizationTargetScopeType.allpkgs.title=All Packages
@@ -301,7 +302,7 @@
 addAuthorizationPkgRule.pkgName.title=Package Name
 addAuthorizationPkgRule.pkgName.required=The package name is required.
 addAuthorizationPkgRule.pkgName.pattern=The package name is malformed.
-addAuthorizationPkgRule.pkgName.notfound=There's no package with the supplied name. +addAuthorizationPkgRule.pkgName.notfound=There is no package with the supplied name.
 addAuthorizationPkgRule.action.title=Add Rule
 addAuthorizationPkgRule.conflict.title=Conflict
addAuthorizationPkgRule.conflict.description=The rule that you tried to add conflicts with a rule that already \
@@ -317,6 +318,7 @@
 banner.action.repositories=List package repositories
 banner.action.users=List users
 banner.action.authorizationPkgRules=Package authorization
+banner.action.pkgFeedBuilder=Build Feed URL

 naturalLanguage.en=English
 naturalLanguage.de=Deutsch
@@ -350,6 +352,31 @@
 userRatingStability.mostlystable.title=Mostly stable
 userRatingStability.stable.title=Stable

+# These are used in the ATOM feed itself for the title of the feed
+feed.createdUserRating.atom.title={0} : user rating from {1}
+feed.createdPkgVersion.atom.title={0} : new version
+
+# These are used in the user-interface to describe the source of the feed items
+feed.source.createdpkgversion.title=Package Version
+feed.source.createduserrating.title=User Rating
+
+# These are used in the page where the user is able to specify what they want in
+# the feed.
+pkgFeedBuilder.limit.title=Max Items
+pkgFeedBuilder.pkgs.title=Packages
+pkgFeedBuilder.pkgs.all=All packages
+pkgFeedBuilder.pkgs.addAction.title=Add
+pkgFeedBuilder.pkgChooserName.placeholder=Name
+pkgFeedBuilder.pkgChooserName.pattern=The package name is malformed.
+pkgFeedBuilder.pkgChooserName.notfound=There is no package with the supplied name.
+pkgFeedBuilder.supplierTypes.title=Inputs
+pkgFeedBuilder.action.title=Build Feed URL
+pkgFeedBuilder.editAction.title=Edit feed URL settings
+pkgFeedBuilder.openFeedAction.title=Open feed in a new window
+pkgFeedBuilder.info=This application is able to generate an ATOM feed that consists of updates \ + from the Haiku-OS packages. Specify what you want to receive and then generate a URL to use \
+  with your preferred feed-aggregation software.
+
 # Test case

 test.it=Test line for integration testing
=======================================
--- /haikudepotserver-webapp/src/main/resources/messages_de.properties Wed Aug 6 10:50:47 2014 UTC +++ /haikudepotserver-webapp/src/main/resources/messages_de.properties Thu Aug 14 11:03:33 2014 UTC
@@ -39,6 +39,7 @@
 breadcrumb.completePasswordReset.title=Kennwort zurücksetzen
 breadcrumb.listAuthorizationPkgRules.title=Paket Autorisierungsregeln
 breadcrumb.addAuthorizationPkgRule.title=Regel hinzufügen
+breadcrumb.pkgFeedBuilder.title=Feed-URL erzeugen

 about.info.title=Information
 about.info.version=Version {0}
@@ -117,7 +118,6 @@
 home.viewCriteriaType.featured=Gekennzeichnet
 home.viewCriteriaType.all=Alphabetisch
 home.viewCriteriaType.categories=Kategorien
-home.viewCriteriaType.featured=Empfehlung
home.viewCriteriaType.mostrecent=Neu aktualisiert; sortiert nach Versionsdatum home.viewCriteriaType.mostviewed=Neu aktualisiert; sortiert nach am meisten angesehen
 home.searchButton.title=Los
@@ -309,6 +309,7 @@
 banner.action.repositories=Paket-Depots anzeigen
 banner.action.users=Benutzer anzeigen
 banner.action.authorizationPkgRules=Paket-Autorisierung
+banner.action.pkgFeedBuilder=Feed-URL erzeugen

 permission.pkg_editicon.title=Paket-Icon bearbeiten
 permission.pkg_editscreenshot.title=Paket-Screenshots bearbeiten
@@ -344,6 +345,32 @@

 rating.none=Noch ohne Bewertung

+# These are used in the ATOM feed itself for the title of the feed
+feed.createdUserRating.atom.title={0} : Bewertung von {1}
+feed.createdPkgVersion.atom.title={0} : Neue Version
+
+# These are used in the user-interface to describe the source of the feed items
+feed.source.createdpkgversion.title=Paketversion
+feed.source.createduserrating.title=Bewertung
+
+# These are used in the page where the user is able to specify what they want in
+# the feed.
+pkgFeedBuilder.limit.title=Max. Einträge
+pkgFeedBuilder.pkgs.title=Pakete
+pkgFeedBuilder.pkgs.all=Alle Pakete
+pkgFeedBuilder.pkgs.addAction.title=Hinzufügen
+pkgFeedBuilder.pkgChooserName.placeholder=Name
+pkgFeedBuilder.pkgChooserName.pattern=Der Paketname ist nicht richtig formatiert. +pkgFeedBuilder.pkgChooserName.notfound=Es existiert kein Paket mit dem angegebenen Namen.
+pkgFeedBuilder.supplierTypes.title=Quellen
+pkgFeedBuilder.action.title=Feed-URL erzeugen
+pkgFeedBuilder.editAction.title=Feed-URL Eigenschaften bearbeiten
+pkgFeedBuilder.openFeedAction.title=Feed in neuem Fenster öffnen
+pkgFeedBuilder.info=Mit dieser Anwendung lassen sich ATOM-Feeds von Haiku Paket-Updates \
+  erzeugen. Einfach auswählen welche Infos empfangen werden sollen, dann \
+  die entsprechende URL erzeugen, die dann mit einer Feed-Sammel-Software \
+  genutzt werden kann.
+
 # Test case

 test.it=Testzeile zum Test der Vollständigkeit
=======================================
--- /haikudepotserver-webapp/src/main/resources/spring/general.xml Sun Aug 3 11:11:48 2014 UTC +++ /haikudepotserver-webapp/src/main/resources/spring/general.xml Thu Aug 14 11:03:33 2014 UTC
@@ -82,4 +82,11 @@
         </property>
     </bean>

+    <!-- LOCALIZATION -->
+
+ <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
+        <property name="defaultEncoding" value="UTF-8"/>
+        <property name="basename" value="classpath:messages"/>
+    </bean>
+
 </beans>
=======================================
--- /haikudepotserver-webapp/src/main/webapp/WEB-INF/web.xml Sun Aug 3 11:11:48 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/WEB-INF/web.xml Thu Aug 14 11:03:33 2014 UTC
@@ -1,10 +1,17 @@

-<web-app id="WebApp_ID" version="2.4"
- xmlns="http://java.sun.com/xml/ns/j2ee"; xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
-         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
-http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd";>
+<web-app
+        xmlns="http://java.sun.com/xml/ns/javaee";
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd";
+        version="3.0">

-    <display-name>Spring Web MVC Application</display-name>
+    <display-name>Haiku Depot Web</display-name>
+
+    <servlet>
+        <servlet-name>error-servlet</servlet-name>
+ <servlet-class>org.haikuos.haikudepotserver.support.web.ErrorServlet</servlet-class>
+        <load-on-startup>1</load-on-startup>
+    </servlet>

     <servlet>
         <servlet-name>jawr-servlet-css</servlet-name>
@@ -52,6 +59,11 @@
         <load-on-startup>1</load-on-startup>
     </servlet>

+    <servlet-mapping>
+        <servlet-name>error-servlet</servlet-name>
+        <url-pattern>/error</url-pattern>
+    </servlet-mapping>
+
     <servlet-mapping>
         <servlet-name>jawr-servlet-css</servlet-name>
         <url-pattern>/jawr/css/*</url-pattern>
@@ -85,19 +97,13 @@
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
     </filter>

-    <filter>
-        <filter-name>errorFilter</filter-name>
- <filter-class>org.haikuos.haikudepotserver.support.web.ErrorFilter</filter-class>
-    </filter>
-
     <filter-mapping>
         <filter-name>authenticationFilter</filter-name>
         <url-pattern>/*</url-pattern>
     </filter-mapping>

-    <filter-mapping>
-        <filter-name>errorFilter</filter-name>
-        <url-pattern>/error</url-pattern>
-    </filter-mapping>
+    <error-page>
+        <location>/error</location>
+    </error-page>

 </web-app>
=======================================
--- /haikudepotserver-webapp/src/main/webapp/css/viewpkg.css Sun Aug 3 11:11:48 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/css/viewpkg.css Thu Aug 14 11:03:33 2014 UTC
@@ -1,6 +1,10 @@
 #pkg-title {
     margin-bottom: 6px;
 }
+
+#pkg-title-feed {
+    float: right;
+}

 #pkg-title-icon {
     display: inline;
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkg.html Wed Aug 6 09:46:44 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkg.html Thu Aug 14 11:03:33 2014 UTC
@@ -3,6 +3,9 @@
 <div class="content-container">

     <div id="pkg-title">
+        <div id="pkg-title-feed">
+            <img src="/img/feed.svg" ng-click="goPkgFeedBuilder()">
+        </div>
         <div id="pkg-title-icon">
             <pkg-icon size="32" pkg="pkg"></pkg-icon>
         </div>
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js Wed Aug 6 09:46:44 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewpkgcontroller.js Thu Aug 14 11:03:33 2014 UTC
@@ -139,10 +139,10 @@
                 $scope.pkgIconHvifUrl = undefined;

                 jsonRpc.call(
-                        constants.ENDPOINT_API_V1_PKG,
-                        "getPkgIcons",
-                        [{ pkgName: $routeParams.name }]
-                    ).then(
+                    constants.ENDPOINT_API_V1_PKG,
+                    "getPkgIcons",
+                    [{ pkgName: $routeParams.name }]
+                ).then(
                     function(result) {

                         var has = !!_.findWhere(
@@ -192,11 +192,11 @@
                             // trim the comments down a bit if necessary.

                             _.each($scope.userRatings.items, function(ur) {
-                                 if(ur.comment) {
- if(ur.comment.length > MAXCHARS_USERRATING_COMMENT) { - ur.comment = ur.comment.substring(0,MAXCHARS_USERRATING_COMMENT) + '...';
-                                     }
-                                 }
+                                if(ur.comment) {
+ if(ur.comment.length > MAXCHARS_USERRATING_COMMENT) { + ur.comment = ur.comment.substring(0,MAXCHARS_USERRATING_COMMENT) + '...';
+                                    }
+                                }
                             });

// trim down the number of lines in the comment if necessary.
@@ -247,10 +247,10 @@
                 $scope.pkgScreenshots = undefined;

                 jsonRpc.call(
-                        constants.ENDPOINT_API_V1_PKG,
-                        "getPkgScreenshots",
-                        [{ pkgName: $scope.pkg.name }]
-                    ).then(
+                    constants.ENDPOINT_API_V1_PKG,
+                    "getPkgScreenshots",
+                    [{ pkgName: $scope.pkg.name }]
+                ).then(
                     function(result) {
$scope.pkgScreenshots = _.map(result.items, function(item) {
                             return {
@@ -294,9 +294,15 @@
             // ---------------------
             // ACTIONS FOR PACKAGE

+            $scope.goPkgFeedBuilder = function() {
+                var item = breadcrumbFactory.createPkgFeedBuilder();
+ breadcrumbFactory.applySearch(item, { pkgNames : $scope.pkg.name });
+                breadcrumbs.pushAndNavigate(item);
+            };
+
             $scope.goListPkgVersions = function() {
breadcrumbs.pushAndNavigate(breadcrumbFactory.createListPkgVersionsForPkg($scope.pkg));
-            }
+            };

// this is used to cause an authentication in relation to adding a user rating
             $scope.goAuthenticate = function() {
@@ -321,14 +327,14 @@

             $scope.goEditPkgProminence = function() {
breadcrumbs.pushAndNavigate(breadcrumbFactory.createEditPkgProminence($scope.pkg));
-            }
+            };

             $scope.goRemoveIcon = function() {
                 jsonRpc.call(
-                        constants.ENDPOINT_API_V1_PKG,
-                        "removePkgIcon",
-                        [{ pkgName: $routeParams.name }]
-                    ).then(
+                    constants.ENDPOINT_API_V1_PKG,
+                    "removePkgIcon",
+                    [{ pkgName: $routeParams.name }]
+                ).then(
                     function() {
$log.info('removed icons for '+$routeParams.name+' pkg');
                         refetchPkgIconMetaData();
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/directive/banner.html Thu Jul 24 11:35:00 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/directive/banner.html Thu Aug 14 11:03:33 2014 UTC
@@ -91,6 +91,12 @@
                 </a>
             </li>

+            <li>
+                <a href="" ng-click="goPkgFeedBuilder()">
+                    <message key="banner.action.pkgFeedBuilder"></message>
+                </a>
+            </li>
+
<li ng-show="canGoListUsers()" show-if-permission="'USER_LIST'">
                 <a href="" ng-click="goListUsers()">
                     <message key="banner.action.users"></message>
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/directive/bannerdirective.js Thu Jul 24 11:35:00 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/directive/bannerdirective.js Thu Aug 14 11:03:33 2014 UTC
@@ -154,6 +154,18 @@
                     $scope.goShowActions = function() {
                         $scope.showActions = true;
                     };
+
+                    // -----------------
+                    // FEEDS
+
+                    $scope.goPkgFeedBuilder = function() {
+                        breadcrumbs.resetAndNavigate([
+                            breadcrumbFactory.createHome(),
+                            breadcrumbFactory.createPkgFeedBuilder()
+                        ]);
+
+                        $scope.showActions = false;
+                    }

                     // -----------------
                     // REPOSITORY
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/routes.js Wed Aug 6 09:46:44 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/routes.js Thu Aug 14 11:03:33 2014 UTC
@@ -11,6 +11,7 @@
var pkgVersionPrefix = '/pkg/:name/:major/:minor?/:micro?/:preRelease?/:revision?/:architectureCode';

             $routeProvider
+                
.when('/pkg/feed/builder',{controller:'PkgFeedBuilderController',templateUrl:'/js/app/controller/pkgfeedbuilder.html'})
                 
.when('/paginationcontrolplayground',{controller:'PaginationControlPlayground',templateUrl:'/js/app/controller/paginationcontrolplayground.html'})
                 
.when('/repositories/add',{controller:'AddEditRepositoryController',templateUrl:'/js/app/controller/addeditrepository.html'})
                 
.when('/repository/:code/edit',{controller:'AddEditRepositoryController',templateUrl:'/js/app/controller/addeditrepository.html'})
=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/service/breadcrumbfactoryservice.js Wed Aug 6 09:46:44 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/service/breadcrumbfactoryservice.js Thu Aug 14 11:03:33 2014 UTC
@@ -394,6 +394,13 @@
                     return applyDefaults({
titleKey : 'breadcrumb.addAuthorizationPkgRule.title',
                         path : '/authorizationpkgrules/add'
+                    });
+                },
+
+                createPkgFeedBuilder : function() {
+                    return applyDefaults({
+                        titleKey : 'breadcrumb.pkgFeedBuilder.title',
+                        path : '/pkg/feed/builder'
                     });
                 }

=======================================
--- /haikudepotserver-webapp/src/main/webapp/js/app/service/referencedataservice.js Wed Aug 6 09:46:44 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/service/referencedataservice.js Thu Aug 14 11:03:33 2014 UTC
@@ -84,6 +84,25 @@

             return {

+                /**
+ * <p>This relates to the ATOM feed sources and although it is hard-coded, it is still + * supplied from this reference data service in order to maintain consistency and to
+                 * allow for easier enhancement later.</p>
+                 */
+
+                feedSupplierTypes : function() {
+                    var deferred = $q.defer();
+                    deferred.resolve(_.map(
+                        [ 'CREATEDPKGVERSION', 'CREATEDUSERRATING' ],
+                        function(item) {
+                            return {
+                                code : item
+                            };
+                        }
+                    ));
+                    return deferred.promise;
+                },
+
                 naturalLanguages : function() {
                     return getData('naturalLanguages');
                 },
=======================================
--- /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/api1/MiscelaneousApiIT.java Thu Jul 24 11:35:00 2014 UTC +++ /haikudepotserver-webapp/src/test/java/org/haikuos/haikudepotserver/api1/MiscelaneousApiIT.java Thu Aug 14 11:03:33 2014 UTC
@@ -5,20 +5,29 @@

 package org.haikuos.haikudepotserver.api1;

+import com.google.common.base.Joiner;
 import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import org.apache.cayenne.ObjectContext;
 import org.fest.assertions.Assertions;
 import org.haikuos.haikudepotserver.AbstractIntegrationTest;
+import org.haikuos.haikudepotserver.IntegrationTestSupportService;
 import org.haikuos.haikudepotserver.api1.model.miscellaneous.*;
+import org.haikuos.haikudepotserver.api1.support.ObjectNotFoundException;
 import org.haikuos.haikudepotserver.dataobjects.NaturalLanguage;
 import org.haikuos.haikudepotserver.dataobjects.PkgCategory;
+import org.haikuos.haikudepotserver.feed.controller.FeedController;
 import org.haikuos.haikudepotserver.support.RuntimeInformationService;
 import org.junit.Test;

 import javax.annotation.Resource;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.util.List;
+import java.util.Map;

 public class MiscelaneousApiIT extends AbstractIntegrationTest {

@@ -41,7 +50,7 @@

Assertions.assertThat(pkgCategories.size()).isEqualTo(result.pkgCategories.size());

-        for(int i=0;i<pkgCategories.size();i++) {
+        for (int i = 0; i < pkgCategories.size(); i++) {
             PkgCategory pkgCategory = pkgCategories.get(i);
GetAllPkgCategoriesResult.PkgCategory apiPkgCategory = result.pkgCategories.get(i); Assertions.assertThat(pkgCategory.getName()).isEqualTo(apiPkgCategory.name);
@@ -62,7 +71,7 @@

Assertions.assertThat(naturalLanguages.size()).isEqualTo(result.naturalLanguages.size());

-        for(int i=0;i<naturalLanguages.size();i++) {
+        for (int i = 0; i < naturalLanguages.size(); i++) {
             NaturalLanguage naturalLanguage = naturalLanguages.get(i);
GetAllNaturalLanguagesResult.NaturalLanguage apiNaturalLanguage = result.naturalLanguages.get(i); Assertions.assertThat(naturalLanguage.getName()).isEqualTo(apiNaturalLanguage.name);
@@ -125,9 +134,36 @@
         // not sure what architectures there may be in the future, but
         // we will just check for a couple that we know to be there.

- Assertions.assertThat(isPresent(result,"x86").isPresent()).isTrue(); - Assertions.assertThat(isPresent(result,"x86_gcc2").isPresent()).isTrue(); - Assertions.assertThat(isPresent(result,"mips").isPresent()).isFalse(); + Assertions.assertThat(isPresent(result, "x86").isPresent()).isTrue(); + Assertions.assertThat(isPresent(result, "x86_gcc2").isPresent()).isTrue(); + Assertions.assertThat(isPresent(result, "mips").isPresent()).isFalse();
+    }
+
+    @Test
+ public void testGenerateFeedUrl() throws ObjectNotFoundException, MalformedURLException { + IntegrationTestSupportService.StandardTestData data = integrationTestSupportService.createStandardTestData();
+
+        GenerateFeedUrlRequest request = new GenerateFeedUrlRequest();
+        request.limit = 55;
+        request.naturalLanguageCode = NaturalLanguage.CODE_GERMAN;
+ request.pkgNames = ImmutableList.of(data.pkg1.getName(), data.pkg2.getName()); + request.supplierTypes = ImmutableList.of(GenerateFeedUrlRequest.SupplierType.CREATEDPKGVERSION);
+
+        // ------------------------------------
+        String urlString = miscellaneousApi.generateFeedUrl(request).url;
+        // ------------------------------------
+
+        URL url = new URL(urlString);
+
+        Assertions.assertThat(url.getPath()).endsWith("/feed/pkg.atom");
+
+        // this is a bit rough, but will do for assertion...
+ Map<String,String> queryParams = Splitter.on('&').trimResults().withKeyValueSeparator('=').split(url.getQuery()); + Assertions.assertThat(queryParams.get(FeedController.KEY_LIMIT)).isEqualTo("55"); + Assertions.assertThat(queryParams.get(FeedController.KEY_NATURALLANGUAGECODE)).isEqualTo(NaturalLanguage.CODE_GERMAN); + Assertions.assertThat(queryParams.get(FeedController.KEY_PKGNAMES)).isEqualTo(Joiner.on('-').join(ImmutableList.of(data.pkg1.getName(), data.pkg2.getName()))); + Assertions.assertThat(queryParams.get(FeedController.KEY_TYPES)).isEqualTo(GenerateFeedUrlRequest.SupplierType.CREATEDPKGVERSION.name());
+
     }

 }
=======================================
--- /haikudepotserver-webapp/src/test/resources/local.properties Sat Jul 12 08:36:41 2014 UTC +++ /haikudepotserver-webapp/src/test/resources/local.properties Thu Aug 14 11:03:33 2014 UTC
@@ -16,6 +16,6 @@
 authentication.jws.issuer=integrationtest.hds

 passwordreset.ttlhours=1
-passwordreset.baseurl=http://www.haiku-os.org/integration-test/pwdr/
+baseurl=https://doesnotexist.haiku-os.org/

 email.from=integration-test-sender@xxxxxxxxxxxx

Other related posts:

  • » [haiku-depot-web] [haiku-depot-web-app] push by haiku.li...@xxxxxxxxx - implement atom feed on 2014-08-14 11:11 GMT - haiku-depot-web-app