/**
* (C) Copyright 2013 Jabylon (http://www.jabylon.org) 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
*/
package org.jabylon.rest.ui.wicket.panels;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import javax.inject.Inject;
import javax.servlet.http.HttpServletResponse;
import org.apache.wicket.AttributeModifier;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.Session;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.CheckBox;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.IFormSubmitter;
import org.apache.wicket.markup.html.form.StatelessForm;
import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.markup.repeater.RepeatingView;
import org.apache.wicket.model.AbstractReadOnlyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.model.StringResourceModel;
import org.apache.wicket.request.http.flow.AbortWithHttpErrorCodeException;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.eclipse.emf.cdo.util.CommitException;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.jabylon.cdo.connector.Modification;
import org.jabylon.cdo.connector.TransactionUtil;
import org.jabylon.common.review.ReviewParticipant;
import org.jabylon.common.util.URLUtil;
import org.jabylon.properties.Comment;
import org.jabylon.properties.Project;
import org.jabylon.properties.ProjectLocale;
import org.jabylon.properties.ProjectVersion;
import org.jabylon.properties.PropertiesFactory;
import org.jabylon.properties.Property;
import org.jabylon.properties.PropertyFile;
import org.jabylon.properties.PropertyFileDescriptor;
import org.jabylon.properties.Review;
import org.jabylon.properties.ReviewState;
import org.jabylon.properties.Severity;
import org.jabylon.resources.persistence.PropertyPersistenceService;
import org.jabylon.rest.ui.Activator;
import org.jabylon.rest.ui.model.PropertyPair;
import org.jabylon.rest.ui.security.CDOAuthenticatedSession;
import org.jabylon.rest.ui.security.RestrictedComponent;
import org.jabylon.rest.ui.util.GlobalResources;
import org.jabylon.rest.ui.util.WebContextUrlResourceReference;
import org.jabylon.rest.ui.wicket.BasicResolvablePanel;
import org.jabylon.rest.ui.wicket.components.AnchorBookmarkablePageLink;
import org.jabylon.security.CommonPermissions;
import org.jabylon.users.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
public class PropertyEditorSinglePanel extends BasicResolvablePanel<PropertyFileDescriptor> implements RestrictedComponent {
private static final long serialVersionUID = 1L;
private static final Logger logger = LoggerFactory.getLogger(PropertyEditorSinglePanel.class);
IModel<Multimap<String, Review>> reviewModel;
IModel<PropertyPair> previousModel;
IModel<PropertyPair> mainModel;
IModel<PropertyPair> nextModel;
EditKind editKind = EditKind.EDIT;
private int index, total;
private PropertyListMode mode;
private String targetKey;
@Inject
List<ReviewParticipant> reviewParticipants;
public PropertyEditorSinglePanel(PropertyFileDescriptor object, PageParameters parameters) {
super("content", object, parameters);
targetKey = fixKeyName(parameters.get("key").toString(null));
}
private String fixKeyName(String string) {
//workaround for https://github.com/jutzig/jabylon/issues/211
if(string!=null && string.endsWith("../"))
return string.substring(0, string.length()-1);
return string;
}
@Override
protected void construct() {
super.construct();
editKind = computeEditKind();
List<ReviewParticipant> activeReviews = PropertyListModeFactory.filterActiveReviews(getModel().getObject().getProjectLocale().getParent().getParent(),reviewParticipants);
mode = PropertyListModeFactory.allAsMap(activeReviews).get(getPageParameters().get("mode").toString());
addLinkList(activeReviews,mode);
reviewModel = new LoadableDetachableModel<Multimap<String, Review>>() {
private static final long serialVersionUID = 1L;
@Override
protected Multimap<String, Review> load() {
return buildReviewMap(getModelObject());
}
};
createModels(getModel(), targetKey, mode);
}
private EditKind computeEditKind() {
ProjectVersion version = getModel().getObject().getProjectLocale().getParent();
if (version.isReadOnly())
return EditKind.READONLY;
Session session = getSession();
if (session instanceof CDOAuthenticatedSession) {
Project project = version.getParent();
CDOAuthenticatedSession authSession = (CDOAuthenticatedSession) session;
if (authSession.hasPermission(CommonPermissions.constructPermission(CommonPermissions.PROJECT, project.getName(), CommonPermissions.ACTION_EDIT)))
return EditKind.EDIT;
if (authSession
.hasPermission(CommonPermissions.constructPermission(CommonPermissions.PROJECT, project.getName(), CommonPermissions.ACTION_SUGGEST)))
return EditKind.SUGGEST;
}
return EditKind.READONLY;
}
@Override
public void renderHead(IHeaderResponse response) {
response.render(JavaScriptHeaderItem.forReference(GlobalResources.JS_WARN_WHEN_DIRTY));
response.render(JavaScriptHeaderItem.forReference(GlobalResources.JS_SHORTCUTS));
response.render(JavaScriptHeaderItem.forReference(new WebContextUrlResourceReference("js/singlePropertyEditor.js")));
super.renderHead(response);
}
private void createModels(IModel<PropertyFileDescriptor> model, String targetKey, PropertyListMode mode) {
PropertyFileDescriptor descriptor = model.getObject();
boolean isTemplateOnly = descriptor.isMaster();
Multimap<String, Review> reviews = reviewModel.getObject();
PropertyFileDescriptor master = isTemplateOnly ? descriptor : descriptor.getMaster();
Map<String, Property> translated = new HashMap<String, Property>(loadProperties(descriptor).asMap());
PropertyFile templateFile = loadProperties(master);
total = templateFile.getProperties().size();
PropertyPair previous = null;
PropertyPair main = null;
PropertyPair next = null;
index = 1;
for (Property property : templateFile.getProperties()) {
Property translation = translated.remove(property.getKey());
if (translation == null)
translation = PropertiesFactory.eINSTANCE.createProperty();
translation.setKey(property.getKey());
PropertyPair pair = new PropertyPair(EcoreUtil.copy(property), EcoreUtil.copy(translation), isTemplateOnly ? ProjectLocale.TEMPLATE_LOCALE : descriptor.getVariant(), descriptor.cdoID());
String key = pair.getKey();
if (mode.apply(pair, reviews.get(key))) {
if (main != null) {
// we already found a hit, this is to compute the next
next = pair;
break;
} else if (targetKey == null || (key != null && key.equals(targetKey))) {
// this is the one we need to edit
main = pair;
} else {
// remember the last one
previous = pair;
}
}
if (main == null)
index++;
}
// we didn't find a next yet, so keep searching in the ones missing in
// the template
if (next == null) {
for (Property property : translated.values()) {
PropertyPair pair = new PropertyPair(null, EcoreUtil.copy(property), isTemplateOnly ? ProjectLocale.TEMPLATE_LOCALE : descriptor.getVariant(), descriptor.cdoID());
if (mode.apply(pair, reviews.get(pair.getKey()))) {
if (main != null) {
// we already found a hit, this is to compute the next
next = pair;
break;
} else if (targetKey == null || (pair.getKey() != null && pair.getKey().equals(targetKey))) {
// this is the one we need to edit
main = pair;
} else {
// remember the last one
previous = pair;
}
}
if (main == null)
index++;
}
}
if (previous != null)
previousModel = Model.of(previous);
if (main != null) {
mainModel = Model.of(main);
} else {
String message = "Property with key {0} was not found in mode {1}";
message = MessageFormat.format(message, targetKey, mode);
logger.info(message);
throw new AbortWithHttpErrorCodeException(HttpServletResponse.SC_NOT_FOUND, message);
}
if (next != null)
nextModel = Model.of(next);
buildComponentTree(previous, main, next);
}
private static PropertyFile loadProperties(PropertyFileDescriptor descriptor) {
try {
PropertyFile propertyFile = getPropertyPersistence().loadProperties(descriptor);
return propertyFile;
} catch (ExecutionException e) {
logger.error("Failed to load property file for " + descriptor, e);
throw new AbortWithHttpErrorCodeException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to load property file for " + descriptor);
}
}
private static PropertyPersistenceService getPropertyPersistence() {
return Activator.getDefault().getPersistenceService();
}
private void buildComponentTree(PropertyPair previous, final PropertyPair main, PropertyPair next) {
boolean isTemplateOnly = main.getLanguage()==ProjectLocale.TEMPLATE_LOCALE;
Form<Property> pairForm = new PropertySubmitForm("properties-form", Model.of(main.getTranslation()), reviewModel, previousModel, mainModel, nextModel,
getModel(), editKind);
add(pairForm);
IModel<String> saveLabel = editKind == EditKind.EDIT ? nls("PropertyEditorSinglePanel.save.button") : nls("PropertyEditorSinglePanel.suggest.button");
Button saveButton = new Button("save");
saveButton.setVisibilityAllowed(editKind != EditKind.READONLY);
saveButton.add(new Label("label", saveLabel));
pairForm.add(saveButton);
Button resetButton = new Button("reset");
resetButton.setVisibilityAllowed(editKind != EditKind.READONLY);
pairForm.add(resetButton);
Button nextButton = new Button("next");
Button previousButton = new Button("previous");
String nextButtonLabelKey = "PropertyEditorSinglePanel.next.button";
String previousButtonLabelKey = "PropertyEditorSinglePanel.previous.button";
switch (editKind) {
case EDIT:
break;
case SUGGEST:
nextButtonLabelKey += ".suggest";
previousButtonLabelKey += ".suggest";
break;
case READONLY:
nextButtonLabelKey += ".readonly";
previousButtonLabelKey += ".readonly";
break;
}
nextButton.add(new Label("label", nls(nextButtonLabelKey)));
previousButton.add(new Label("label", nls(previousButtonLabelKey)));
Property template = main.getTemplate();
String key = null;
if (template != null)
key = main.getTemplate().getKey();
else
key = main.getTranslation().getKey();
final Label icon = new Label("icon");
icon.setOutputMarkupId(true);
pairForm.add(icon);
final WebMarkupContainer templatePanel = new WebMarkupContainer("template-area");
templatePanel.setOutputMarkupId(true);
pairForm.add(templatePanel);
if(isTemplateOnly)
templatePanel.setVisible(false);
final WebMarkupContainer translationPanel = new WebMarkupContainer("translation-area");
translationPanel.setOutputMarkupId(true);
pairForm.add(translationPanel);
final Label translationLabel = new Label("translation-label", new PropertyModel<PropertyPair>(main, "translated"));
pairForm.add(new Label("key-label", key));
pairForm.add(translationLabel);
TextArea<PropertyPair> textArea = new TextArea<PropertyPair>("template", new PropertyModel<PropertyPair>(main, "original"));
templatePanel.add(textArea);
textArea.setEnabled(editKind != EditKind.READONLY);
textArea = new TextArea<PropertyPair>("template-comment", new PropertyModel<PropertyPair>(main, "originalComment"));
templatePanel.add(textArea);
textArea = new TextArea<PropertyPair>("translation-comment", new PropertyModel<PropertyPair>(main, "translatedComment"));
translationPanel.add(textArea);
if (editKind == EditKind.READONLY)
textArea.add(new AttributeModifier("readonly", "readonly"));
if(isTemplateOnly) {
textArea.add(new AttributeModifier("class", "span12"));
}
textArea = new TextArea<PropertyPair>("translation", new PropertyModel<PropertyPair>(main, "translated"));
textArea.add(new AttributeModifier("lang", new AbstractReadOnlyModel<String>() {
private static final long serialVersionUID = 1L;
@Override
public String getObject() {
// see http://www.ietf.org/rfc/bcp/bcp47.txt
return main.getLanguage().toString().replace('_', '-');
}
}));
if(isTemplateOnly) {
textArea.add(new AttributeModifier("class", "span12"));
}
textArea.add(new AttributeModifier("translate", "no"));
if (editKind == EditKind.READONLY)
textArea.add(new AttributeModifier("readonly", "readonly"));
translationPanel.add(textArea);
pairForm.add(nextButton);
pairForm.add(previousButton);
String progressLabel = new StringResourceModel("translation.progress", this, null, index, total).getObject();
progressLabel = MessageFormat.format(progressLabel, index, total);
Label progress = new Label("progress", String.valueOf(progressLabel));
double actualIndex = Math.max(1, index);
int percent = (int) ((actualIndex / total) * 100);
progress.add(new AttributeModifier("style", "width: " + percent + "%"));
pairForm.add(progress);
fillReviewsColumn(mainModel, pairForm);
PropertiesTools tools = new PropertiesTools("tools", mainModel, new PageParameters());
add(tools);
}
private Multimap<String, Review> buildReviewMap(PropertyFileDescriptor object) {
EList<Review> reviews = object.getReviews();
Multimap<String, Review> reviewMap = ArrayListMultimap.create(reviews.size(), 2);
for (Review review : reviews) {
reviewMap.put(review.getKey(), review);
}
return reviewMap;
}
private void addLinkList(List<ReviewParticipant> activeReviews, final PropertyListMode currentMode)
{
List<PropertyListMode> values = PropertyListModeFactory.all(activeReviews);
ListView<PropertyListMode> mode = new ListView<PropertyListMode>("view-mode", values) {
private static final long serialVersionUID = 1L;
@Override
protected void populateItem(ListItem<PropertyListMode> item) {
String mode = item.getModelObject().name().toLowerCase();
String anchor = URLUtil.escapeToIdAttribute(mainModel.getObject().getKey());
PageParameters pageParams = new PageParameters(getPageParameters()).clearNamed().set("mode", mode);
BookmarkablePageLink<Object> link = new AnchorBookmarkablePageLink<Object>("link", getPage().getClass(), pageParams, anchor);
ReviewParticipant participant = item.getModel().getObject().getParticipant();
if(participant!=null)
link.setBody(nls(participant.getClass(),participant.getName()));
else
link.setBody(new StringResourceModel(item.getModelObject().name(),item,null));
item.add(link);
link.add(new AttributeModifier("onclick", "return confirmAction()"));
if (item.getModelObject().equals(currentMode))
item.add(new AttributeModifier("class", "active"));
}
};
add(mode);
}
@Override
public String getRequiredPermission() {
Project project = getModel().getObject().getProjectLocale().getParent().getParent();
return CommonPermissions.constructPermission(CommonPermissions.PROJECT, project.getName(), CommonPermissions.ACTION_VIEW);
}
protected void fillReviewsColumn(IModel<PropertyPair> propertyPair, MarkupContainer container) {
RepeatingView view = new RepeatingView("reviews");
container.add(view);
if (propertyPair == null || propertyPair.getObject() == null)
return;
Collection<Review> reviews = reviewModel.getObject().get(propertyPair.getObject().getKey());
DateFormat formatter = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.SHORT, getSession().getLocale());
for (Review review : reviews) {
if (review.getState() == ReviewState.INVALID || review.getState() == ReviewState.RESOLVED)
continue;
Label label = new Label(view.newChildId(), review.getReviewType());
label.add(new AttributeAppender("class", getLabelClass(review)));
if(editKind==EditKind.EDIT && Review.KIND_SUGGESTION.equals(review.getReviewType())) {
//allow the label to be clicked to apply the suggestion
label.add(new AttributeAppender("data-suggestion", review.getMessage()));
}
StringBuilder title = new StringBuilder();
if (review.getMessage() != null)
title.append(review.getMessage());
if (review.getCreated() > 0) {
if (title.length() > 0)
// add a linebreak
title.append("\n");
title.append(formatter.format(new Date(review.getCreated())));
}
if (title.length() > 0)
label.add(new AttributeModifier("title", title.toString()));
view.add(label);
}
}
protected String getLabelClass(Review review) {
Severity severity = review.getSeverity();
switch (severity) {
case ERROR:
return " label-important";
case INFO:
return " label-info";
case WARNING:
return " label-warning";
default:
return "";
}
}
private static enum EditKind {
READONLY, SUGGEST, EDIT;
}
private static class PropertySubmitForm extends StatelessForm<Property> {
private static final long serialVersionUID = 1L;
private IModel<Multimap<String, Review>> reviewModel;
private IModel<PropertyPair> previousModel;
private IModel<PropertyPair> mainModel;
private IModel<PropertyPair> nextModel;
private IModel<PropertyFileDescriptor> descriptorModel;
private IModel<Boolean> modified;
private EditKind editKind = EditKind.EDIT;
public PropertySubmitForm(String id, IModel<Property> model, IModel<Multimap<String, Review>> reviewModel, IModel<PropertyPair> previousModel,
IModel<PropertyPair> mainModel, IModel<PropertyPair> nextModel, IModel<PropertyFileDescriptor> descriptorModel, EditKind editKind) {
super(id, model);
this.reviewModel = reviewModel;
this.previousModel = previousModel;
this.mainModel = mainModel;
this.nextModel = nextModel;
this.descriptorModel = descriptorModel;
this.modified = Model.of(Boolean.valueOf(false));
;
this.editKind = editKind;
// this will let us know if the user actually changed anything
CheckBox modifiedIndicator = new CheckBox("modify-indicator", modified);
modifiedIndicator.setDefaultModelObject(false);
add(modifiedIndicator);
}
@Override
protected void onSubmit() {
super.onSubmit();
doSubmit();
}
protected void doSubmit() {
IFormSubmitter submitter = findSubmittingButton();
if (submitter instanceof Button && ((Button) submitter).getId().equals("reset")) {
setResponsePage(getPage().getClass(), getPageParameters().set("key", mainModel.getObject().getKey()));
return;
}
if (modified.getObject()) {
if (editKind == EditKind.EDIT) {
PropertyFileDescriptor descriptor = descriptorModel.getObject();
PropertyFile file = loadProperties(descriptor);
Map<String, Property> map = file.asMap();
Property translation = getModelObject();
if (translation != null) {
if (map.containsKey(translation.getKey())) {
Property property = map.get(translation.getKey());
property.setComment(translation.getComment());
property.setValue(translation.getValue());
} else if (!isEmpty(translation.getValue())) {
file.getProperties().add(translation);
}
}
if (translation.getValue() != null) {
// see if we can close a manual review because we
// accepted the suggestion
Multimap<String, Review> reviewMap = reviewModel.getObject();
Collection<Review> reviews = reviewMap.get(translation.getKey());
for (Review review : reviews) {
if (review.getState() == ReviewState.OPEN && Review.KIND_SUGGESTION.equals(review.getReviewType())) {
if (translation.getValue().equals(review.getMessage())) {
try {
TransactionUtil.commit(review, new Modification<Review, Review>() {
@Override
public Review apply(Review object) {
object.setState(ReviewState.RESOLVED);
return object;
}
});
} catch (CommitException e) {
logger.error("Failed to commit review resolution", e);
}
}
}
}
}
getPropertyPersistence().saveProperties(descriptor, file);
/*
* see https://github.com/jutzig/jabylon/issues/112 for now
* we deactivate the successful message to not bother the
* user getSession().info("Saved successfully");
*/
} else if (editKind == EditKind.SUGGEST) {
Property translation = getModelObject();
PropertyFileDescriptor descriptor = descriptorModel.getObject();
final Review review = PropertiesFactory.eINSTANCE.createReview();
review.setCreated(System.currentTimeMillis());
review.setKey(translation.getKey());
review.setMessage(translation.getValue());
review.setSeverity(Severity.INFO);
review.setReviewType(Review.KIND_SUGGESTION);
String username = CommonPermissions.USER_ANONYMOUS;
Session session = getSession();
if (session instanceof CDOAuthenticatedSession) {
CDOAuthenticatedSession authSession = (CDOAuthenticatedSession) session;
User user = authSession.getUser();
if (user != null)
username = user.getName();
}
review.setUser(username);
if (translation.getComment() != null && !translation.getComment().isEmpty()) {
Comment comment = PropertiesFactory.eINSTANCE.createComment();
comment.setUser(username);
comment.setCreated(review.getCreated());
comment.setMessage(translation.getComment());
review.getComments().add(comment);
}
try {
TransactionUtil.commit(descriptor, new Modification<PropertyFileDescriptor, PropertyFileDescriptor>() {
@Override
public PropertyFileDescriptor apply(PropertyFileDescriptor object) {
object.getReviews().add(review);
return object;
}
});
} catch (CommitException e) {
logger.error("Commit of suggestion failed", e);
}
}
}
if (submitter instanceof Button) {
Button button = (Button) submitter;
if (button.getId().equals("next")) {
if (nextModel != null && nextModel.getObject() != null)
setResponsePage(getPage().getClass(), getPageParameters().set("key", nextModel.getObject().getKey()));
else {
// there is no next. go to overview
setResponsePage(getPage().getClass(), getPageParameters().set("key", null));
}
} else if (button.getId().equals("previous")) {
if (previousModel != null && previousModel.getObject() != null)
setResponsePage(getPage().getClass(), getPageParameters().set("key", previousModel.getObject().getKey()));
else {
// there is no next. go to overview
setResponsePage(getPage().getClass(), getPageParameters().set("key", null));
}
} else {
setResponsePage(getPage().getClass(), getPageParameters().set("key", mainModel.getObject().getKey()));
}
}
}
private PageParameters getPageParameters() {
return getPage().getPageParameters();
}
private boolean isEmpty(String string) {
return string == null || string.isEmpty();
}
}
}