/******************************************************************************* * Copyright (c) 2010-2014 SAP AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Distribution License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/edl-v10.html * * Contributors: * SAP AG - initial API and implementation *******************************************************************************/ package org.eclipse.skalli.view.ext.impl.internal.infobox; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.eclipse.skalli.commons.FormatUtils; import org.eclipse.skalli.commons.HtmlBuilder; import org.eclipse.skalli.commons.HtmlUtils; import org.eclipse.skalli.commons.Link; import org.eclipse.skalli.ext.mapping.scm.ScmLocationMapper; import org.eclipse.skalli.model.Project; import org.eclipse.skalli.services.Services; import org.eclipse.skalli.services.feed.Entry; import org.eclipse.skalli.services.feed.FeedManager; import org.eclipse.skalli.services.feed.FeedProvider; import org.eclipse.skalli.services.feed.FeedService; import org.eclipse.skalli.services.feed.FeedUpdater; import org.eclipse.skalli.view.component.FloatLayout; import org.eclipse.skalli.view.ext.ExtensionUtil; import org.eclipse.skalli.view.ext.InfoBox; import org.eclipse.skalli.view.ext.InfoBoxBase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.vaadin.ui.Button; import com.vaadin.ui.Button.ClickEvent; import com.vaadin.ui.CheckBox; import com.vaadin.ui.Component; import com.vaadin.ui.CssLayout; import com.vaadin.ui.Layout; public class FeedInfoBox extends InfoBoxBase implements InfoBox { private static final String CAPTION = "Timeline"; //$NON-NLS-1$ private static final String ICON = "res/icons/feed.png"; //$NON-NLS-1$ private static final String STYLE_TIMELINE_INFOBOX = "infobox-timeline"; //$NON-NLS-1$ private static final String STYLE_TIMELINE_ENTRY = "timeline-entry"; //$NON-NLS-1$ private static final Logger LOG = LoggerFactory.getLogger(FeedInfoBox.class); private FeedService feedService; private Set<FeedProvider> feedProviders = new HashSet<FeedProvider>(); private static final int MAX_DISPLAY_LENGTH_TITLE = 80; private static final int INITAL_MAX_FEED_ENTRIES = 7; private int maxFeedEntries = INITAL_MAX_FEED_ENTRIES; private class SourceDetails { private boolean selected; private String caption; public SourceDetails(boolean selected, String caption) { this.selected = selected; this.caption = caption; } } protected void bindFeedService(FeedService feedService) { this.feedService = feedService; } protected void unbindFeedService(FeedService feedService) { this.feedService = null; } protected void bindFeedProvider(FeedProvider feedProvider) { feedProviders.add(feedProvider); } protected void unbindFeedProvider(FeedProvider feedProvider) { feedProviders.remove(feedProvider); } @Override public String getCaption() { return CAPTION; } @Override public String getShortName() { return "timeline"; //$NON-NLS-1$ } @Override public float getPositionWeight() { // some high value to have it displayed as one of the last extensions return 100; } @Override public int getPreferredColumn() { return COLUMN_EAST; } @Override public boolean isVisible(Project project, String userId) { return feedService != null; } @Override public String getIconPath() { return ICON; } @Override public Component getContent(Project project, ExtensionUtil util) { List<String> sources = getSources(project); if (sources.isEmpty()) { return null; // nothing to render } Layout contentLayout = new CssLayout(); contentLayout.addStyleName(STYLE_TIMELINE_INFOBOX); contentLayout.setSizeFull(); maxFeedEntries = INITAL_MAX_FEED_ENTRIES; HashMap<String, SourceDetails> sourceFilter = new HashMap<String, SourceDetails>(); Map<String, String> captions = getCaptions(project, sources); for (String source : sources) { String caption = captions.get(source); if (StringUtils.isBlank(caption)) { caption = source; } sourceFilter.put(source, new SourceDetails(true, caption)); } renderContentPanel(contentLayout, project, util.getLoggedInUserId(), sourceFilter); return contentLayout; } private void renderContentPanel(Layout layout, Project project, String userId, HashMap<String, SourceDetails> sourceFilter) { layout.removeAllComponents(); renderSourceFilters(layout, project, userId, sourceFilter); renderSubscribeLink(layout, project); renderTimelineContent(layout, project, userId, sourceFilter); } private List<String> getSources(Project project) { try { return feedService.findSources(project.getUuid()); } catch (IOException e) { LOG.error(MessageFormat.format("Failed to retrieve feed sources for project {0}", project.getProjectId()), e); } return Collections.emptyList(); } private void renderSourceFilters(Layout parentLayout, Project project, String userId, HashMap<String, SourceDetails> sourceFilter) { FloatLayout grid = new FloatLayout(); Set<String> keys = sourceFilter.keySet(); for (String source : keys) { addSourceFilter(parentLayout, grid, source, sourceFilter, project, userId); } parentLayout.addComponent(grid); } private void renderSubscribeLink(Layout parentLayout, Project project) { createLink(parentLayout, "Subscribe to Feed", MessageFormat.format("/api/projects/{0}/timeline", //$NON-NLS-1$ project.getProjectId()), DEFAULT_TARGET, STYLE_TIMELINE_ENTRY); } private Map<String, String> getCaptions(Project project, List<String> sources) { Map<String, String> captions = new HashMap<String, String>(); for (FeedProvider feedProvider : feedProviders) { List<FeedUpdater> updaters = feedProvider.getFeedUpdaters(project); for (FeedUpdater updater : updaters) { if (sources.contains(updater.getSource())) { captions.put(updater.getSource(), updater.getCaption()); } } } return captions; } private void renderTimelineContent(Layout layout, Project project, String userId, HashMap<String, SourceDetails> sourceFilter) { try { List<String> selectedSources = getSelecedSources(sourceFilter); if (!CollectionUtils.isEmpty(selectedSources)) { List<Entry> entries = feedService.findEntries(project.getUuid(), selectedSources, maxFeedEntries + 1); createLabel(layout, getEntriesAsHtml(project, userId, entries, Math.min(entries.size(), maxFeedEntries)).toString()); if (entries.size() > maxFeedEntries) { addMoreButton(layout, project, userId, sourceFilter); } } } catch (IOException e) { LOG.error(MessageFormat.format("Failed to retrieve feed entries for project {0}", project.getProjectId()), e); } } @SuppressWarnings({ "deprecation", "serial" }) private void addMoreButton(final Layout layout, final Project project, final String userId, final HashMap<String, SourceDetails> sourceFilter) { final Button moreButton = new Button("more ..."); moreButton.setStyle(Button.STYLE_LINK); moreButton.addListener(new Button.ClickListener() { @Override public void buttonClick(ClickEvent event) { maxFeedEntries = maxFeedEntries + 30; renderContentPanel(layout, project, userId, sourceFilter); } }); layout.addComponent(moreButton); } private HtmlBuilder getEntriesAsHtml(Project project, String userId, List<Entry> entries, int maxCount) { HtmlBuilder html = new HtmlBuilder(); for (int i = 0; i < maxCount; i++) { addEntry(project, userId, html, entries.get(i)); } return html; } private List<String> getSelecedSources(HashMap<String, SourceDetails> sourceFilter) { List<String> result = new ArrayList<String>(); Set<String> sources = sourceFilter.keySet(); for (String source : sources) { if (sourceFilter.get(source).selected) { result.add(source); } } return result; } private void addSourceFilter(final Layout parentLayout, FloatLayout layout, final String source, final HashMap<String, SourceDetails> sourceFilter, final Project project, final String userId) { final SourceDetails value = sourceFilter.get(source); CheckBox cb = new CheckBox(value.caption, value.selected); cb.setImmediate(true); cb.addListener(new Button.ClickListener() { private static final long serialVersionUID = 7364120771141334914L; @Override public void buttonClick(ClickEvent event) { boolean checked = event.getButton().booleanValue(); value.selected = checked; renderContentPanel(parentLayout, project, userId, sourceFilter); } }); layout.addComponent(cb, "margin-right:10px;"); //$NON-NLS-1$ } @SuppressWarnings("nls") private void addEntry(Project project, String userId, HtmlBuilder html, Entry entry) { html.append("<p class=\"").append(STYLE_TIMELINE_ENTRY).append("\">"); String title = StringUtils.abbreviate(entry.getTitle(), MAX_DISPLAY_LENGTH_TITLE); String link = null; if (entry.getLink() != null) { link = entry.getLink().getHref(); } link = mapLink(project, userId, link); html.appendLink(HtmlUtils.clean(title), link); html.append("<br />"); String source = entry.getSource(); if (StringUtils.isNotBlank(HtmlUtils.clean(source))) { html.append(source); } String date = getDate(entry); if (StringUtils.isNotBlank(date)) { html.append(" - ").append(HtmlUtils.clean(date)); } String author = getAuthor(entry); if (StringUtils.isNotBlank(author)) { html.append(" - ").append(HtmlUtils.clean(author)); } html.append("</p>"); } private String mapLink(Project project, String userId, String link) { ScmLocationMapper mapper = new ScmLocationMapper(ScmLocationMapper.ALL_PROVIDERS, ScmLocationMapper.PURPOSE_FEED_LINK); List<Link> mappedScmLinks = mapper.getMappedLinks(link, userId, project); if (mappedScmLinks.size() == 0) { if (LOG.isDebugEnabled()) { LOG.debug(MessageFormat.format("no mapping for feed link =''{0}'' with purpose = ''{1}'' defined.", link, ScmLocationMapper.PURPOSE_FEED_LINK)); } return link; } // take the first found mapping return mappedScmLinks.get(0).getUrl(); } private String getDate(Entry entry) { String date = null; Date published = entry.getPublished(); if (published != null) { date = FormatUtils.formatUTC(published.getTime()); } return date; } private String getAuthor(Entry entry) { HtmlBuilder author = new HtmlBuilder(); if (entry.getAuthor() != null) { String link = null; String caption = null; if (StringUtils.isNotBlank(entry.getAuthor().getName())) { caption = entry.getAuthor().getName(); } if (StringUtils.isNotBlank(entry.getAuthor().getEmail())) { link = entry.getAuthor().getEmail(); if (caption == null) { caption = link; } } author.appendMailToLink(null, link, caption); } return author.toString(); } @Override public void perform(String action, Project project, String userId) { if (REFRESH_ACTION.equalsIgnoreCase(action)) { try { FeedManager feedManager = Services.getService(FeedManager.class); if (feedManager != null) { feedManager.updateFeeds(project.getUuid()); } else { LOG.error(MessageFormat.format( "Failed to perform \''{0}\'' action on project \''{1}\'' for user \''{2}\'': No feed manager available", action, project.getUuid(), userId)); } } catch (Exception e) { LOG.error(MessageFormat.format("Failed to perform \''{0}\'' action on project \''{1}\'' for user \''{2}\''", action, project.getUuid(), userId), e); } } } }