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(" → "); + 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(" → "); - 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; + @Overridepublic 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 NicknameaddAuthorizationPkgRule.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 PackageaddAuthorizationPkgRule.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=ConflictaddAuthorizationPkgRule.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=Empfehlunghome.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