/** * Copyright (c) 2011-2012, Thilo Planz. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package v7cr; import static org.apache.commons.lang.ArrayUtils.EMPTY_STRING_ARRAY; import java.io.File; import java.io.IOException; import java.text.DateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.time.DurationFormatUtils; import org.bson.types.ObjectId; import org.tmatesoft.svn.core.SVNLogEntry; import org.tmatesoft.svn.core.SVNLogEntryPath; import org.vaadin.easyuploads.MultiFileUpload; import v7cr.v7db.BSONBackedObject; import v7cr.v7db.LocalizedString; import v7cr.v7db.SchemaDefinition; import com.vaadin.terminal.ExternalResource; import com.vaadin.terminal.ThemeResource; import com.vaadin.ui.Button; import com.vaadin.ui.Component; import com.vaadin.ui.ComponentContainer; import com.vaadin.ui.CustomComponent; import com.vaadin.ui.GridLayout; import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.Label; import com.vaadin.ui.Link; import com.vaadin.ui.OptionGroup; import com.vaadin.ui.Panel; import com.vaadin.ui.TabSheet; import com.vaadin.ui.TextArea; import com.vaadin.ui.Tree; import com.vaadin.ui.VerticalLayout; import com.vaadin.ui.Button.ClickEvent; import com.vaadin.ui.Button.ClickListener; import com.vaadin.ui.Window.Notification; @SuppressWarnings("serial") public class ReviewTab extends CustomComponent implements ClickListener { final ObjectId reviewId; private Review r; private TextArea newComment; private OptionGroup voteOptions; private ComponentContainer fileArea; ReviewTab(ObjectId id) { setIcon(new ThemeResource("../runo/icons/16/document-txt.png")); reviewId = id; reload(); } private void reload() { final V7CR v7 = V7CR.getInstance(); r = new Review(v7.load("reviews", reviewId)); Project p = new Project(v7.load("projects", r.getProjectName())); SVNLogEntry svn = r.getSVNLogEntry(); String url; if (svn != null) { url = r.getProjectName() + "-" + svn.getRevision(); setCaption(url); } else { url = reviewId.toString(); setCaption(StringUtils.abbreviate(r.getTitle(), 20)); } VerticalLayout vl = new VerticalLayout(); vl.setSizeFull(); vl.addComponent(getBasicInfo(v7, r, p, url)); Panel s = getSVNPanel(v7, r.getSchemaDefinition(), svn, p); if (s != null) vl.addComponent(s); final BSONBackedObject[] notes = r.getObjectFieldAsArray("notes"); if (notes != null) { for (BSONBackedObject note : notes) { vl.addComponent(getNotesPanel(note)); } } final BSONBackedObject[] votes = r.getObjectFieldAsArray("v"); if (votes != null) { for (BSONBackedObject vote : votes) { vl.addComponent(new CommentPanel(vote)); } } { HorizontalLayout commentGrid = new HorizontalLayout(); newComment = new TextArea(); newComment.setColumns(50); newComment.setRows(10); commentGrid.addComponent(newComment); voteOptions = new OptionGroup(); voteOptions.addItem("+"); voteOptions.addItem("0"); voteOptions.addItem("-"); voteOptions.setValue("0"); commentGrid.addComponent(voteOptions); vl.addComponent(commentGrid); fileArea = new VerticalLayout(); vl.addComponent(fileArea); MultiFileUpload uploader = new MultiFileUpload() { @Override protected void handleFile(File file, String fileName, String mimeType, long length) { try { BSONBackedObject gf = v7.storeFile(file, fileName, mimeType); TemporaryFile tf = new TemporaryFile(v7, gf); fileArea.addComponent(tf); } catch (IOException e) { throw new RuntimeException(e); } } }; vl.addComponent(uploader); Button submitButton = new Button(v7.getMessage("button.submit")); submitButton.addListener(this); vl.addComponent(submitButton); } setCompositionRoot(vl); Component parent = getParent(); if (parent instanceof TabSheet) { TabSheet t = (TabSheet) parent; Iterator<Component> i = t.getComponentIterator(); while (i.hasNext()) { Component c = i.next(); if (c instanceof ReviewList && r.getProjectName().equals(c.getCaption())) { ((ReviewList) c).reload(); break; } } } } class CommentPanel extends CustomComponent { private final Panel p; private final BSONBackedObject data; CommentPanel(BSONBackedObject vote) { p = new Panel(); data = vote; p.setWidth("600px"); makeNotEditable(); } private void makeNotEditable() { final GridLayout grid = new GridLayout(3, 4); grid.setSizeFull(); p.setContent(grid); grid.setSpacing(true); Date created = data.getDateField("d"); p.addComponent(new Label(data.getStringField("by.n"))); p.addComponent(new Label(data.getStringField("by._id"))); p.addComponent(new Label(DateFormat.getDateTimeInstance().format( created))); grid.addComponent(new Label(data.getStringField("c"), Label.CONTENT_PREFORMATTED), 0, 1, 1, 1); Label icon = new Label(); String v = data.getStringField("v"); if ("+".equals(v)) { icon.setIcon(new ThemeResource("../runo/icons/32/ok.png")); } if ("-".equals(v)) { icon .setIcon(new ThemeResource( "../runo/icons/32/attention.png")); } p.addComponent(icon); setCompositionRoot(p); V7CR v7 = V7CR.getInstance(); BSONBackedObject[] files = data.getObjectFieldAsArray("files"); if (files != null) { for (BSONBackedObject f : files) { Link downloadLink = v7.getFile(f); if (downloadLink != null) grid.addComponent(downloadLink, 0, grid.getCursorY(), 2, grid.getCursorY()); } } // the last comment can still be edited for some time long timeLeft = created.getTime() + 30 * 60 * 1000 - System.currentTimeMillis(); if (timeLeft > 0) { p.addComponent(new Button(v7.getMessage("button.edit"), new Button.ClickListener() { public void buttonClick(ClickEvent event) { makeEditable(); } } )); p.addComponent(new Button(v7.getMessage("button.delete"), new Button.ClickListener() { public void buttonClick(ClickEvent event) { V7CR v7cr = V7CR.getInstance(); v7cr.update("reviews", r.deleteVote(data)); reload(); } })); p.addComponent(new Label(DurationFormatUtils .formatDurationWords(timeLeft, true, true) + " left to edit")); } ; } private void makeEditable() { final GridLayout grid = new GridLayout(3, 4); grid.setSizeFull(); p.setContent(grid); grid.setSpacing(true); V7CR v7 = V7CR.getInstance(); final Date created = data.getDateField("d"); p.addComponent(new Label(data.getStringField("by.n"))); p.addComponent(new Label(data.getStringField("by._id"))); p.addComponent(new Label(DateFormat.getDateTimeInstance().format( created))); final TextArea textArea = new TextArea(null, data .getStringField("c")); textArea.setSizeFull(); grid.addComponent(textArea, 0, 1, 1, 1); final OptionGroup voteOptions = new OptionGroup(); voteOptions.addItem("+"); voteOptions.addItem("0"); voteOptions.addItem("-"); voteOptions.setValue("0"); grid.addComponent(voteOptions); p.addComponent(new Button(v7.getMessage("button.submit"), new Button.ClickListener() { public void buttonClick(ClickEvent event) { V7CR v7cr = V7CR.getInstance(); v7cr.update("reviews", r.updateVote(data, textArea .getValue().toString(), voteOptions .getValue().toString())); reload(); // // V7CR.getInstance().getDBCollection("reviews").update( // new BasicDBObject("_id", reviewId).append("v.d", // created), // new BasicDBObject("$set", new BasicDBObject( // "v.$.c", textArea.getValue()).append( // "v.$.v", voteOptions.getValue()))); } } )); p.addComponent(new Button(v7.getMessage("button.cancel"), new Button.ClickListener() { public void buttonClick(ClickEvent event) { makeNotEditable(); } })); p.addComponent(new Button(v7.getMessage("button.delete"), new Button.ClickListener() { public void buttonClick(ClickEvent event) { V7CR v7cr = V7CR.getInstance(); v7cr.update("reviews", r.deleteVote(data)); reload(); } })); } } private Panel getBasicInfo(V7CR v7, Review r, Project proj, String linkUrl) { Panel p = new Panel(v7.getMessage("reviewTab.review")); p.setWidth("600px"); GridLayout grid = new GridLayout(3, 4); grid.setSizeFull(); p.setContent(grid); grid.setSpacing(true); Locale l = v7.getLocale(); SchemaDefinition sd = r.getSchemaDefinition(); grid.addComponent(new Label(sd.getFieldCaption("s", l)), 0, 0, 1, 0); p.addComponent(new Label("<b>" + LocalizedString.get(sd.getFieldDefinition("s") .getPossibleValueMetaData(r.getStatus()), "caption", l) + "</b>", Label.CONTENT_XHTML)); p.addComponent(new Label(sd.getFieldCaption("p", l))); p.addComponent(new Label("[" + proj.getId() + "]")); grid.addComponent(new Label(proj.getName())); p.addComponent(new Label(sd.getFieldCaption("reviewee", l))); p.addComponent(new Label(r.getReviewee().getId())); grid.addComponent(new Label(r.getReviewee().getName())); p.addComponent(new Label(sd.getFieldCaption("t", l))); grid.addComponent(new Label(r.getTitle()), 1, 3, 2, 3); p.addComponent(new Label(v7.getMessage("reviewTab.directLink"))); Link link = new Link(linkUrl, new ExternalResource(linkUrl)); link.setTargetName("_blank"); link.setIcon(new ThemeResource("../runo/icons/16/arrow-right.png")); grid.addComponent(link); return p; } private Panel getNotesPanel(BSONBackedObject note) { Panel p = new Panel(note.getStringField("t")); p.setWidth("600px"); Label c = new Label(note.getStringField("c")); p.addComponent(c); String v = note.getStringField("v"); if ("+".equals(v)) { c.setIcon(new ThemeResource("../runo/icons/16/ok.png")); } if ("-".equals(v)) { c.setIcon(new ThemeResource("../runo/icons/16/attention.png")); } return p; } private Panel getSVNPanel(V7CR v7, SchemaDefinition sd, SVNLogEntry svn, Project proj) { if (svn == null) return null; Locale l = v7.getLocale(); Panel p = new Panel(v7.getMessage("reviewTab.subversion")); p.setWidth("600px"); GridLayout grid = new GridLayout(4, 4); grid.setSizeFull(); p.setContent(grid); grid.setSpacing(true); p.addComponent(new Label(sd.getFieldCaption("svn.rev", l))); p.addComponent(new Label("" + svn.getRevision())); p.addComponent(new Label(DateFormat.getDateTimeInstance().format( svn.getDate()))); p.addComponent(new Label(svn.getAuthor())); Link link = new Link(v7.getMessage("reviewTab.viewChanges"), new ExternalResource(proj .getChangesetViewUrl(svn.getRevision()))); link.setTargetName("_blank"); link.setIcon(new ThemeResource("../runo/icons/16/arrow-right.png")); p.addComponent(link); grid.addComponent(new Label(svn.getMessage()), 1, 1, 3, 1); Map<String, SVNLogEntryPath> changed = svn.getChangedPaths(); if (changed != null) { Tree changeTree = new Tree(sd.getFieldCaption("svn.changed", l) + "(" + changed.size() + ")"); Set<String> paths = changed.keySet(); for (String s : changed.keySet()) { changeTree.addItem(s); changeTree.setChildrenAllowed(s, false); changeTree .setItemCaption(s, changed.get(s).getType() + " " + s); } if (paths.size() > 5) { compressTree(changeTree, paths); } grid.addComponent(changeTree, 0, 2, 3, 2); } return p; } private String connectPath(Tree tree, String node) { tree.addItem(node); tree.setChildrenAllowed(node, true); String pp = StringUtils.substringBeforeLast(StringUtils .substringBeforeLast(node, "/"), "/") + "/"; while (true) { if (tree.containsId(pp)) { tree.setParent(node, pp); return pp; } pp = StringUtils.substringBeforeLast(StringUtils .substringBeforeLast(pp, "/"), "/") + "/"; if ("/".equals(pp)) return pp; } } // introduce intermediate nodes (for common prefixes) to make the tree more // even private void compressTree(Tree tree, Collection<String> nodes) { List<String> orgPaths = new ArrayList<String>(nodes); int end = orgPaths.size(); for (int start = 0; start < end; start++) { String prefix = StringUtils.getCommonPrefix(orgPaths.subList(start, end).toArray(EMPTY_STRING_ARRAY)); if (!prefix.endsWith("/")) { prefix = StringUtils.substringBeforeLast(prefix, "/") + "/"; } String first = orgPaths.get(start); String extendedPrefix = prefix + StringUtils.substringBefore(first.substring(prefix .length()), "/"); if (!extendedPrefix.equals(first)) { extendedPrefix += "/"; if (tree.containsId(prefix)) { tree.addItem(extendedPrefix); tree.setParent(extendedPrefix, prefix); } else { tree.setParent(extendedPrefix, connectPath(tree, extendedPrefix)); } tree.setChildrenAllowed(extendedPrefix, true); int last; for (last = start; last < end; last++) { String l = orgPaths.get(last); if (l.startsWith(extendedPrefix)) { tree.setParent(l, extendedPrefix); } else { last++; break; } } tree.setItemCaption(extendedPrefix, extendedPrefix + "(" + (last - 1 - start) + ")"); if (last - start > 5) { compressTree(tree, orgPaths.subList(start, last - 1)); } start = last - 1; } else { if (!tree.containsId(prefix)) { prefix = connectPath(tree, prefix); } if (!prefix.equals(first)) { tree.setParent(first, prefix); } } } } public void buttonClick(ClickEvent event) { String comment = (String) newComment.getValue(); String vote = (String) voteOptions.getValue(); if (StringUtils.isBlank(comment)) { getWindow().showNotification(null, "Please leave a comment", Notification.TYPE_WARNING_MESSAGE); return; } if (StringUtils.isBlank(vote)) { getWindow().showNotification(null, "Please vote", Notification.TYPE_WARNING_MESSAGE); return; } V7CR v7cr = V7CR.getInstance(); v7cr.update("reviews", r.addVote(v7cr.getSessionUser(), new Date(), comment, vote, fileArea)); reload(); } }