/******************************************************************************* * 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 Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * SAP AG - initial API and implementation *******************************************************************************/ package org.eclipse.skalli.view.ext.impl.internal.infobox; import java.util.ArrayList; import java.util.List; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringUtils; import org.eclipse.skalli.commons.HtmlUtils; import org.eclipse.skalli.model.Project; import org.eclipse.skalli.model.User; import org.eclipse.skalli.model.ext.misc.ProjectRating; import org.eclipse.skalli.model.ext.misc.ProjectRatingStyle; import org.eclipse.skalli.model.ext.misc.ReviewEntry; import org.eclipse.skalli.model.ext.misc.ReviewProjectExt; import org.eclipse.skalli.view.ext.ExtensionUtil; import com.vaadin.terminal.ThemeResource; import com.vaadin.ui.Alignment; import com.vaadin.ui.Button; import com.vaadin.ui.Button.ClickEvent; import com.vaadin.ui.Component; import com.vaadin.ui.CssLayout; import com.vaadin.ui.CustomComponent; import com.vaadin.ui.Embedded; import com.vaadin.ui.GridLayout; import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.Label; import com.vaadin.ui.Layout; import com.vaadin.ui.OptionGroup; import com.vaadin.ui.TextField; import com.vaadin.ui.VerticalLayout; import com.vaadin.ui.Window; public class ReviewComponent extends CustomComponent { private static final long serialVersionUID = 5778729327534523225L; private final ThemeResource ICON_THUMB_UP = new ThemeResource("icons/rating/thumb-up.png"); //$NON-NLS-1$ private final ThemeResource ICON_THUMB_DOWN = new ThemeResource("icons/rating/thumb-down.png"); //$NON-NLS-1$ private final ThemeResource ICON_FACE_CRYING = new ThemeResource("icons/rating/face-crying.png"); //$NON-NLS-1$ private final ThemeResource ICON_FACE_SAD = new ThemeResource("icons/rating/face-sad.png"); //$NON-NLS-1$ private final ThemeResource ICON_FACE_PLAIN = new ThemeResource("icons/rating/face-plain.png"); //$NON-NLS-1$ private final ThemeResource ICON_FACE_SMILE = new ThemeResource("icons/rating/face-smile.png"); //$NON-NLS-1$ private final ThemeResource ICON_FACE_SMILE_BIG = new ThemeResource("icons/rating/face-smile-big.png"); //$NON-NLS-1$ private final ThemeResource ICON_BUTTON_OK = new ThemeResource("icons/button/ok.png"); //$NON-NLS-1$ private final ThemeResource ICON_BUTTON_CANCEL = new ThemeResource("icons/button/cancel.png"); //$NON-NLS-1$ private static final int MILLIS_PER_SECOND = 1000; private static final int MILLIS_PER_MINUTE = MILLIS_PER_SECOND * 60; private static final int MILLIS_PER_HOUR = MILLIS_PER_MINUTE * 60; private static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24; private static final String HSPACE = "    "; //$NON-NLS-1$ private Project project; private ReviewProjectExt extension; private List<ReviewEntry> reviews; private int size; private int defaultPageLength; private int maxPageLength; private int currentPageLength; private boolean showAll; private ExtensionUtil util; private ProjectRatingStyle ratingStyle; private Layout layout; private GridLayout reviewGrid; private Button morelessButton; private Button nextButton; private Button prevButton; private CssLayout reviewButtons; private Window reviewPopup; private Label ratioLabel; private int currentPage; private int lastPage; @SuppressWarnings("serial") public ReviewComponent(Project project, int defaultPageLength, int maxPageLength, ExtensionUtil util) { this.project = project; this.defaultPageLength = defaultPageLength; this.maxPageLength = maxPageLength; this.util = util; extension = project.getExtension(ReviewProjectExt.class); if (extension == null) { extension = new ReviewProjectExt(); extension.setExtensibleEntity(project); project.addExtension(extension); } reviews = extension.getReviews(); ratingStyle = extension.getRatingStyle(); showAll = false; size = reviews.size(); currentPage = 0; currentPageLength = defaultPageLength; lastPage = size / maxPageLength; layout = new CssLayout() { @Override protected String getCss(Component c) { if (c instanceof Button) { return "padding-top:3px;padding-right:15px"; //$NON-NLS-1$ } if (c instanceof Label) { return "padding-bottom:10px"; //$NON-NLS-1$ } return "padding-top:3px"; //$NON-NLS-1$ } }; layout.setSizeFull(); paintReviewButtons(); paintReviewList(); setCompositionRoot(layout); } private void paintReviewList() { if (size > 0) { painRatioLabel(); paintReviewGrid(); paintPageButtons(); } } private void painRatioLabel() { String ratioLabelValue = null; if (ProjectRatingStyle.TWO_STATES.equals(ratingStyle)) { ratioLabelValue = "<span style=\"font-weight:bold\">" //$NON-NLS-1$ + extension.getRecommendedRatio() + " of " + extension.getNumberVotes() + " users recommend this project" + "</span>"; //$NON-NLS-1$ } else if (ProjectRatingStyle.FIVE_STATES.equals(ratingStyle)) { ratioLabelValue = "<span style=\"font-weight:bold\">" //$NON-NLS-1$ + "Average rating of this project by " + extension.getNumberVotes() + " users: " + getDescription(extension.getAverageRating()) + "</span>"; //$NON-NLS-1$ } if (ratioLabel == null) { ratioLabel = new Label(ratioLabelValue, Label.CONTENT_XHTML); layout.addComponent(ratioLabel); } else { ratioLabel.setValue(ratioLabelValue); } } private void paintReviewGrid() { if (reviewGrid == null) { reviewGrid = new GridLayout(2, currentPageLength); reviewGrid.setSizeFull(); reviewGrid.setSpacing(true); layout.addComponent(reviewGrid); } else { reviewGrid.removeAllComponents(); } int rows = reviewGrid.getRows(); if (rows != currentPageLength) { reviewGrid.setRows(Math.min(size, currentPageLength)); } int row = 0; List<ReviewEntry> latestReviews = getLatestReviews(currentPage, currentPageLength); for (ReviewEntry review : latestReviews) { ProjectRating rating = review.getRating(); Embedded e = new Embedded(null, getIcon(rating)); e.setDescription(getDescription(rating)); e.setWidth("22px"); //$NON-NLS-1$ e.setHeight("22px"); //$NON-NLS-1$ reviewGrid.addComponent(e, 0, row); StringBuilder sb = new StringBuilder(); sb.append("<span style=\"white-space:normal\">"); //$NON-NLS-1$ sb.append(HtmlUtils.clean(review.getComment())); sb.append("<br>"); //$NON-NLS-1$ sb.append("<span style=\"font-size:x-small\">").append(" posted by "); //$NON-NLS-1$ sb.append(StringEscapeUtils.escapeHtml(review.getVoter())); sb.append(" "); //$NON-NLS-1$ long deltaMillis = System.currentTimeMillis() - review.getTimestamp(); long deltaDays = deltaMillis / MILLIS_PER_DAY; if (deltaDays > 0) { sb.append(deltaDays); sb.append(" days ago"); } else { long deltaHours = deltaMillis / MILLIS_PER_HOUR; if (deltaHours > 0) { sb.append(deltaHours).append(" hours ago"); } else { long deltaMinutes = deltaMillis / MILLIS_PER_MINUTE; if (deltaMinutes > 0) { sb.append(deltaMinutes).append(" minutes ago"); } else { sb.append(" just now"); } } sb.append("</span></span>"); //$NON-NLS-1$ } CssLayout css = new CssLayout(); css.setSizeFull(); Label comment = new Label(sb.toString(), Label.CONTENT_XHTML); comment.setSizeUndefined(); css.addComponent(comment); reviewGrid.addComponent(css, 1, row); reviewGrid.setColumnExpandRatio(1, 1.0f); ++row; } } private ThemeResource getIcon(ProjectRating rating) { ThemeResource icon = null; switch (rating) { case UP: icon = ICON_THUMB_UP; break; case DOWN: icon = ICON_THUMB_DOWN; break; case FACE_CRYING: icon = ICON_FACE_CRYING; break; case FACE_SAD: icon = ICON_FACE_SAD; break; case FACE_PLAIN: icon = ICON_FACE_PLAIN; break; case FACE_SMILE: icon = ICON_FACE_SMILE; break; case FACE_SMILE_BIG: icon = ICON_FACE_SMILE_BIG; break; } return icon; } private String getDescription(ProjectRating rating) { String description = null; switch (rating) { case UP: description = "Recommended"; break; case DOWN: description = "Not Recommended"; break; case FACE_CRYING: description = "Lousy!"; break; case FACE_SAD: description = "Poor"; break; case FACE_PLAIN: description = "Average"; break; case FACE_SMILE: description = "Good"; break; case FACE_SMILE_BIG: description = "Excellent!"; break; } return description; } private String getRatingQuestion() { switch (ratingStyle) { case TWO_STATES: return "Would you recommend this project?"; case FIVE_STATES: return "How would you rate this project?"; } return null; } private String getReviewButtonsHeight() { switch (ratingStyle) { case TWO_STATES: return "80px"; //$NON-NLS-1$ case FIVE_STATES: return "70px"; //$NON-NLS-1$ } return "0px"; //$NON-NLS-1$ } private String getReviewComment(ProjectRating rating) { String comment = null; switch (rating) { case UP: comment = "I recommend this project!"; break; case DOWN: comment = "I do not recommend this project!"; break; case FACE_CRYING: comment = "I think this project is lousy!"; break; case FACE_SAD: comment = "I think this project is poor!"; break; case FACE_PLAIN: comment = "I think this project is average!"; break; case FACE_SMILE: comment = "I think this project is good!"; break; case FACE_SMILE_BIG: comment = "I think this project is excellent!"; break; } return comment; } private String getReviewCommentQuestion(ProjectRating rating) { String question = null; switch (rating) { case UP: question = "Why do you recommend this project?"; break; case DOWN: question = "Why do you not recommend this project?"; break; case FACE_CRYING: question = "Why do you think this project is lousy?"; break; case FACE_SAD: question = "Why do you think this project is poor?"; break; case FACE_PLAIN: question = "Why do you think this project is average?"; break; case FACE_SMILE: question = "Why do you think this project is good?"; break; case FACE_SMILE_BIG: question = "Why do you think this project is excellent?"; break; } return question; } private void paintPageButtons() { paintMoreLessButton(); paintPrevNextButtons(); paintButtonStates(); } @SuppressWarnings({ "deprecation", "serial" }) private void paintMoreLessButton() { if (morelessButton == null) { morelessButton = new Button(); morelessButton.setStyleName(Button.STYLE_LINK); morelessButton.addListener(new Button.ClickListener() { @Override public void buttonClick(ClickEvent event) { if (showAll) { showAll = false; currentPage = 0; currentPageLength = defaultPageLength; lastPage = size / currentPageLength; paintButtonStates(); } else { showAll = true; currentPage = 0; currentPageLength = maxPageLength; lastPage = size / currentPageLength; paintButtonStates(); } paintReviewGrid(); } }); layout.addComponent(morelessButton); } } @SuppressWarnings({ "serial", "deprecation" }) private void paintPrevNextButtons() { if (prevButton == null) { prevButton = new Button(); prevButton.setStyleName(Button.STYLE_LINK); prevButton.addListener(new Button.ClickListener() { @Override public void buttonClick(ClickEvent event) { --currentPage; paintReviewGrid(); paintButtonStates(); } }); layout.addComponent(prevButton); } if (nextButton == null) { nextButton = new Button(); nextButton.setStyleName(Button.STYLE_LINK); nextButton.addListener(new Button.ClickListener() { @Override public void buttonClick(ClickEvent event) { ++currentPage; paintReviewGrid(); paintButtonStates(); } }); layout.addComponent(nextButton); } } private void paintButtonStates() { String caption = showAll ? "Show Latest Reviews" : "Show All Reviews"; morelessButton.setCaption(caption); if (showAll || size > currentPageLength) { morelessButton.setEnabled(true); } else { morelessButton.setEnabled(false); } if (showAll && size > currentPageLength) { nextButton.setVisible(true); nextButton.setCaption("Next " + currentPageLength + " Reviews"); prevButton.setVisible(true); prevButton.setCaption("Previous " + currentPageLength + " Reviews"); if (currentPage == lastPage || size <= currentPageLength) { nextButton.setEnabled(false); } else { nextButton.setEnabled(true); } if (currentPage == 0 || size <= currentPageLength) { prevButton.setEnabled(false); } else { prevButton.setEnabled(true); } } else { nextButton.setVisible(false); prevButton.setVisible(false); } } @SuppressWarnings("serial") private void paintReviewButtons() { reviewButtons = new CssLayout() { @Override protected String getCss(Component c) { if (c instanceof HorizontalLayout) { return "padding-left: 15px; padding-top: 10px;"; //$NON-NLS-1$ } else { return StringUtils.EMPTY; } } }; reviewButtons.setWidth("300px"); //$NON-NLS-1$ reviewButtons.setHeight(getReviewButtonsHeight()); Label label = new Label("<b>" + getRatingQuestion() + "</b>", Label.CONTENT_XHTML); //$NON-NLS-1$ //$NON-NLS-2$ reviewButtons.addComponent(label); HorizontalLayout hl = new HorizontalLayout(); hl.setSizeUndefined(); final String separatorLabelCaption = "<b>" + HSPACE + "or" + HSPACE + "</b>"; //$NON-NLS-1$ //$NON-NLS-3$ ProjectRating[] ratings = ProjectRating.getRatings(ratingStyle); int i = 0; for (ProjectRating rating : ratings) { if (i > 0) { Label separatorLabel = new Label(separatorLabelCaption, Label.CONTENT_XHTML); hl.addComponent(separatorLabel); hl.setComponentAlignment(separatorLabel, Alignment.MIDDLE_LEFT); } paintReviewButton(hl, rating); ++i; } reviewButtons.addComponent(hl); layout.addComponent(reviewButtons); } @SuppressWarnings({ "serial", "deprecation" }) private void paintReviewButton(HorizontalLayout hl, final ProjectRating rating) { Button btn = new Button(); if (util.getLoggedInUser() != null) { btn.addListener(new Button.ClickListener() { @Override public void buttonClick(ClickEvent event) { reviewPopup = createReviewWindow(rating); getWindow().addWindow(reviewPopup); } }); btn.setDescription(getDescription(rating)); } else { btn.setEnabled(false); btn.setDescription("Login to rate this project."); } btn.setStyleName(Button.STYLE_LINK); btn.setIcon(getIcon(rating)); hl.addComponent(btn); } @SuppressWarnings("serial") private Window createReviewWindow(final ProjectRating rating) { final Window subwindow = new Window("Rate and Review"); subwindow.setModal(true); subwindow.setWidth("420px"); //$NON-NLS-1$ subwindow.setHeight("320px"); //$NON-NLS-1$ VerticalLayout vl = (VerticalLayout) subwindow.getContent(); vl.setSpacing(true); vl.setSizeFull(); HorizontalLayout hl = new HorizontalLayout(); hl.setSizeUndefined(); Embedded icon = new Embedded(null, getIcon(rating)); Label iconLabel = new Label("<b>" + HSPACE + getReviewComment(rating) + "</b>", Label.CONTENT_XHTML); //$NON-NLS-1$ //$NON-NLS-2$ String captionTextField = getReviewCommentQuestion(rating); hl.addComponent(icon); hl.addComponent(iconLabel); hl.setComponentAlignment(iconLabel, Alignment.MIDDLE_LEFT); vl.addComponent(hl); final TextField editor = new TextField(captionTextField); editor.setRows(3); editor.setColumns(30); editor.setImmediate(true); vl.addComponent(editor); final User user = util.getLoggedInUser(); final ArrayList<String> userSelects = new ArrayList<String>(2); userSelects.add("I want to vote as " + user.getDisplayName()); if (extension.getAllowAnonymous()) { userSelects.add("I want to vote as Anonymous!"); } final OptionGroup userSelect = new OptionGroup(null, userSelects); userSelect.setNullSelectionAllowed(false); userSelect.select(userSelects.get(0)); vl.addComponent(userSelect); CssLayout css = new CssLayout() { @Override protected String getCss(Component c) { return "margin-left:5px;margin-right:5px;margin-top:10px"; //$NON-NLS-1$ } }; Button okButton = new Button("OK"); okButton.setIcon(ICON_BUTTON_OK); okButton.setDescription("Commit changes"); okButton.addListener(new Button.ClickListener() { @Override public void buttonClick(ClickEvent event) { String comment = (String) editor.getValue(); if (StringUtils.isBlank(comment)) { comment = "No Comment"; } ((Window) subwindow.getParent()).removeWindow(subwindow); String userName = "Anonymous"; if (userSelects.get(0).equals(userSelect.getValue())) { userName = user.getDisplayName(); } ReviewEntry review = new ReviewEntry(rating, comment, userName, System.currentTimeMillis()); extension.addReview(review); util.persist(project); reviews = extension.getReviews(); size = reviews.size(); currentPage = 0; lastPage = size / currentPageLength; paintReviewList(); } }); css.addComponent(okButton); Button cancelButton = new Button("Cancel"); cancelButton.setIcon(ICON_BUTTON_CANCEL); cancelButton.setDescription("Discard changes"); cancelButton.addListener(new Button.ClickListener() { @Override public void buttonClick(ClickEvent event) { ((Window) subwindow.getParent()).removeWindow(subwindow); } }); css.addComponent(cancelButton); vl.addComponent(css); vl.setComponentAlignment(css, Alignment.MIDDLE_CENTER); return subwindow; } private List<ReviewEntry> getLatestReviews(int currentPage, int len) { int first = size - currentPage * currentPageLength - 1; if (first >= size) { first = size - 1; } int last = first - len + 1; if (last < 0) { last = 0; } ArrayList<ReviewEntry> result = new ArrayList<ReviewEntry>(); for (int i = first; i >= last; --i) { result.add(reviews.get(i)); } return result; } }