/*
* Copyright 2009-2013 Hippo B.V. (http://www.onehippo.com)
* Copyright 2013 Tirasa.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onehippo.taxonomy.plugin;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import javax.jcr.Node;
import javax.swing.tree.TreeNode;
import org.apache.commons.collections.CollectionUtils;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.ResourceReference;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.IAjaxCallDecorator;
import org.apache.wicket.ajax.calldecorator.AjaxPreprocessingCallDecorator;
import org.apache.wicket.ajax.form.OnChangeAjaxBehavior;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.markup.html.CSSPackageResource;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.basic.MultiLineLabel;
import org.apache.wicket.markup.html.form.ChoiceRenderer;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.image.Image;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.RefreshingView;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.validation.validator.StringValidator.MinimumLengthValidator;
import org.hippoecm.frontend.PluginRequestTarget;
import org.hippoecm.frontend.dialog.IDialogService;
import org.hippoecm.frontend.plugin.IPluginContext;
import org.hippoecm.frontend.plugin.config.IPluginConfig;
import org.hippoecm.frontend.service.ISettingsService;
import org.hippoecm.frontend.service.render.RenderPlugin;
import org.hippoecm.frontend.widgets.TextAreaWidget;
import org.hippoecm.frontend.widgets.TextFieldWidget;
import org.hippoecm.repository.api.StringCodec;
import org.hippoecm.repository.api.StringCodecFactory;
import org.onehippo.taxonomy.api.Taxonomy;
import org.onehippo.taxonomy.plugin.api.EditableCategory;
import org.onehippo.taxonomy.plugin.api.EditableCategoryInfo;
import org.onehippo.taxonomy.plugin.api.TaxonomyException;
import org.onehippo.taxonomy.plugin.model.CategoryModel;
import org.onehippo.taxonomy.plugin.model.JcrTaxonomy;
import org.onehippo.taxonomy.plugin.tree.CategoryNode;
import org.onehippo.taxonomy.plugin.tree.TaxonomyNode;
import org.onehippo.taxonomy.plugin.tree.TaxonomyTree;
import org.onehippo.taxonomy.plugin.tree.TaxonomyTreeModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* TaxonomyEditorPlugin used when editing taxonomy documents.
*
* @version $Id: TaxonomyEditorPlugin.java 1459 2013-07-01 08:48:46Z mmilicevic $
*/
@SuppressWarnings("unchecked")
public class TaxonomyEditorPlugin extends RenderPlugin<Node> {
protected static final Logger LOG = LoggerFactory.getLogger(TaxonomyEditorPlugin.class);
private static final long serialVersionUID = -8104500978113391727L;
private List<LanguageSelection> availableLanguageSelections;
private LanguageSelection currentLanguageSelection;
private JcrTaxonomy taxonomy;
private String key;
private String path;
private IModel<String[]> synonymModel;
private Form<?> container;
private MarkupContainer holder;
private MarkupContainer toolbarHolder;
private TaxonomyTree tree;
private final boolean useUrlKeyEncoding;
/**
* Constructor which adds all the UI components.
* The UI components include taxonomy tree, toolbar, and detail form container.
* The detail form container holds all the category detail fields such as name, description and synonyms.
*
* @param context
* @param config
*/
public TaxonomyEditorPlugin(final IPluginContext context, final IPluginConfig config) {
super(context, config);
final boolean editing = "edit".equals(config.getString("mode"));
useUrlKeyEncoding = config.getAsBoolean("keys.urlencode", false);
taxonomy = newTaxonomy(getModel(), editing);
availableLanguageSelections = getAvailableLanguageSelections();
currentLanguageSelection = new LanguageSelection(getLocale(), getLocale());
synonymModel = new IModel<String[]>() {
private static final long serialVersionUID = 1L;
public String[] getObject() {
EditableCategoryInfo info = taxonomy.getCategoryByKey(key).
getInfo(currentLanguageSelection.getLanguageCode());
return info.getSynonyms();
}
public void setObject(String[] object) {
EditableCategoryInfo info = taxonomy.getCategoryByKey(key).
getInfo(currentLanguageSelection.getLanguageCode());
try {
info.setSynonyms(object);
} catch (TaxonomyException e) {
redraw();
}
}
public void detach() {
}
};
final IModel<Taxonomy> taxonomyModel = new Model<Taxonomy>(taxonomy);
String currentLanguageCode = currentLanguageSelection.getLanguageCode();
tree =
new TaxonomyTree("tree", new TaxonomyTreeModel(taxonomyModel, currentLanguageCode), currentLanguageCode) {
private static final long serialVersionUID = 1L;
@Override
public boolean isEnabled() {
return editing;
}
@Override
protected void onNodeLinkClicked(AjaxRequestTarget target, TreeNode node) {
if (node instanceof CategoryNode) {
key = ((CategoryNode) node).getCategory().getKey();
path = ((CategoryNode) node).getCategory().getPath();
} else {
key = null;
path = null;
}
redraw();
super.onNodeLinkClicked(target, node);
}
};
tree.setOutputMarkupId(true);
add(tree);
if (editing) {
TreeNode rootNode = (TreeNode) tree.getModelObject().getRoot();
tree.getTreeState().selectNode(rootNode, true);
}
holder = new WebMarkupContainer("container-holder");
holder.setOutputMarkupId(true);
toolbarHolder = new WebMarkupContainer("toolbar-container-holder");
toolbarHolder.setOutputMarkupId(true);
AjaxLink<Void> addCategory = new AjaxLink<Void>("add-category") {
private static final long serialVersionUID = 1L;
@Override
public boolean isEnabled() {
return editing;
}
@Override
public void onClick(AjaxRequestTarget target) {
IDialogService dialogService = getDialogService();
dialogService.show(new NewCategoryDialog(taxonomyModel, path) {
private static final long serialVersionUID = 1L;
@Override
public boolean useKeyUrlEncoding() {
return useUrlKeyEncoding;
}
@Override
public StringCodec getNodeNameCodec() {
final ISettingsService settingsService = getPluginContext().getService(
ISettingsService.SERVICE_ID, ISettingsService.class);
final StringCodecFactory stringCodecFactory = settingsService.getStringCodecFactory();
final StringCodec stringCodec = stringCodecFactory.getStringCodec("encoding.node");
if (stringCodec == null) {
return new StringCodecFactory.UriEncoding();
}
return stringCodec;
}
@Override
protected void onOk() {
EditableCategory category = taxonomy.getCategoryByKey(key);
TreeNode node;
if (category != null) {
node = new CategoryNode(new CategoryModel(taxonomyModel, key), currentLanguageSelection.
getLanguageCode());
} else {
node = new TaxonomyNode(taxonomyModel, currentLanguageSelection.getLanguageCode());
}
try {
String newKey = getKey();
if (category != null) {
category.addCategory(newKey, getName(), currentLanguageSelection.getLanguageCode());
} else {
taxonomy.addCategory(newKey, getName(), currentLanguageSelection.getLanguageCode());
}
TreeNode child = new CategoryNode(new CategoryModel(taxonomyModel, newKey),
currentLanguageSelection.getLanguageCode());
tree.getTreeState().selectNode(child, true);
key = newKey;
} catch (TaxonomyException e) {
error(e.getMessage());
}
tree.getTreeState().expandNode(node);
tree.markNodeChildrenDirty(node);
redraw();
}
});
}
};
addCategory.add(new Image("add-category-icon", new ResourceReference(TaxonomyEditorPlugin.class,
"res/new-category-16.png")));
if (!editing) {
addCategory.add(new AttributeAppender("class", new Model<String>("disabled"), " "));
}
toolbarHolder.add(addCategory);
// <HCT>
AjaxLink<Void> removeCategory = new AjaxLink<Void>("remove-category") {
private static final long serialVersionUID = 5538299138211283825L;
@Override
protected IAjaxCallDecorator getAjaxCallDecorator() {
return new AjaxPreprocessingCallDecorator(super.getAjaxCallDecorator()) {
private static final long serialVersionUID = -7927968187160354605L;
@Override
public CharSequence preDecorateScript(final CharSequence script) {
return "if (confirm('" + getString("confirmDelete") + "'))" + "{" + script + "}";
}
};
}
@Override
public boolean isEnabled() {
return editing;
}
@Override
public void onClick(final AjaxRequestTarget target) {
EditableCategory category = taxonomy.getCategoryByKey(key);
if (category != null) {
CategoryNode node = (CategoryNode) tree.getTreeState().getSelectedNodes().iterator().next();
TreeNode parent = node.getParent();
try {
category.remove();
key = null;
tree.getTreeState().collapseNode(parent);
tree.getTreeState().expandNode(parent);
redraw();
} catch (TaxonomyException e) {
LOG.error("Could not remove category {} [{}]",
new Object[] { category.getName(), category.getPath(), e });
}
}
}
};
removeCategory.add(new Image("remove-category-icon",
new ResourceReference(TaxonomyEditorPlugin.class, "res/remove-category-16.png")));
if (!editing) {
addCategory.add(new AttributeAppender("class", new Model<String>("disabled"), " "));
}
toolbarHolder.add(removeCategory);
// </HCT>
container = new Form("container") {
private static final long serialVersionUID = 1L;
@Override
public boolean isVisible() {
return taxonomy.getCategoryByKey(key) != null;
}
};
ChoiceRenderer<LanguageSelection> choiceRenderer =
new ChoiceRenderer<LanguageSelection>("displayName", "languageCode");
DropDownChoice<LanguageSelection> languageSelectionChoice =
new DropDownChoice<LanguageSelection>("language", new PropertyModel<LanguageSelection>(this,
"currentLanguageSelection"), availableLanguageSelections, choiceRenderer);
languageSelectionChoice.add(new OnChangeAjaxBehavior() {
private static final long serialVersionUID = -151291731388673682L;
@Override
protected void onUpdate(AjaxRequestTarget target) {
redraw();
}
});
languageSelectionChoice.setOutputMarkupId(true);
languageSelectionChoice.setEnabled(!CollectionUtils.isEmpty(availableLanguageSelections));
container.add(languageSelectionChoice);
// show key value key:
final Label label = new Label("widgetKey", new KeyModel());
container.add(label);
if (editing) {
MarkupContainer name = new Fragment("name", "fragmentname", this);
FormComponent<String> nameField = new TextField<String>("widget", new NameModel());
nameField.add(new OnChangeAjaxBehavior() {
private static final long serialVersionUID = 1L;
@Override
protected void onUpdate(AjaxRequestTarget target) {
tree.markNodeDirty(getSelectedNode());
}
});
name.add(nameField);
container.add(name);
container.add(new TextAreaWidget("description", new DescriptionModel()));
} else {
container.add(new Label("name", new NameModel()));
TextField<String> myKey = new TextField<String>("key");
myKey.setVisible(false);
container.add(myKey);
container.add(new MultiLineLabel("description", new DescriptionModel()));
}
container.add(new RefreshingView<String>("view") {
private static final long serialVersionUID = 1L;
@Override
protected Iterator<IModel<String>> getItemModels() {
return getSynonymList().iterator();
}
@Override
protected void populateItem(final Item<String> item) {
item.add(new AjaxLink("up") {
private static final long serialVersionUID = 1L;
@Override
public boolean isEnabled() {
return item.getIndex() > 0;
}
@Override
public boolean isVisible() {
return editing;
}
@Override
public void onClick(AjaxRequestTarget target) {
String[] synonyms = synonymModel.getObject();
int index = item.getIndex();
String tmp = synonyms[index];
synonyms[index] = synonyms[index - 1];
synonyms[index - 1] = tmp;
synonymModel.setObject(synonyms);
target.addComponent(holder);
}
});
item.add(new AjaxLink("down") {
private static final long serialVersionUID = 1L;
@Override
public boolean isEnabled() {
String[] synonyms = synonymModel.getObject();
return item.getIndex() < synonyms.length - 1;
}
@Override
public boolean isVisible() {
return editing;
}
@Override
public void onClick(AjaxRequestTarget target) {
String[] synonyms = synonymModel.getObject();
int index = item.getIndex();
String tmp = synonyms[index];
synonyms[index] = synonyms[index + 1];
synonyms[index + 1] = tmp;
synonymModel.setObject(synonyms);
target.addComponent(holder);
}
});
item.add(new AjaxLink("remove") {
private static final long serialVersionUID = 1L;
@Override
public boolean isVisible() {
return editing;
}
@Override
public void onClick(AjaxRequestTarget target) {
String[] synonyms = synonymModel.getObject();
String[] syns = new String[synonyms.length - 1];
System.arraycopy(synonyms, 0, syns, 0, item.getIndex());
System.arraycopy(synonyms, item.getIndex() + 1, syns, item.getIndex(), synonyms.length
- item.getIndex() - 1);
synonymModel.setObject(syns);
target.addComponent(holder);
}
});
if (editing) {
TextFieldWidget input = new TextFieldWidget("synonym", item.getModel());
FormComponent fc = (FormComponent) input.get("widget");
fc.add(new MinimumLengthValidator(1));
item.add(input);
} else {
item.add(new Label("synonym", item.getModel()));
}
}
});
container.add(new AjaxLink("add") {
private static final long serialVersionUID = 1L;
@Override
public boolean isVisible() {
return editing;
}
@Override
public void onClick(AjaxRequestTarget target) {
String[] synonyms = synonymModel.getObject();
String[] newSyns = new String[synonyms.length + 1];
System.arraycopy(synonyms, 0, newSyns, 0, synonyms.length);
newSyns[synonyms.length] = "";
synonymModel.setObject(newSyns);
target.addComponent(holder);
}
});
holder.add(container);
add(toolbarHolder);
add(holder);
add(CSSPackageResource.getHeaderContribution(TaxonomyEditorPlugin.class, "res/style.css"));
}
/*
* Copying from org.apache.commons.lang.LocaleUtils#toLocale(String str)
* because this utility has been added since commons-lang-2.4, but
* hippo-cms-engine:jar:2.22.02 is pulling commons-lang-2.1 transitively.
* So, instead of touching the transitive dependency, copy this utility method
* as deprecated. We will remove this later as soon as hippo-cms modules upgrade
* the dependency on commons-lang.
* @deprecated
*/
private static Locale toLocale(String str) {
if (str == null) {
return null;
}
int len = str.length();
if (len != 2 && len != 5 && len < 7) {
throw new IllegalArgumentException("Invalid locale format: " + str);
}
char ch0 = str.charAt(0);
char ch1 = str.charAt(1);
if (ch0 < 'a' || ch0 > 'z' || ch1 < 'a' || ch1 > 'z') {
throw new IllegalArgumentException("Invalid locale format: " + str);
}
if (len == 2) {
return new Locale(str, "");
} else {
if (str.charAt(2) != '_') {
throw new IllegalArgumentException("Invalid locale format: " + str);
}
char ch3 = str.charAt(3);
if (ch3 == '_') {
return new Locale(str.substring(0, 2), "", str.substring(4));
}
char ch4 = str.charAt(4);
if (ch3 < 'A' || ch3 > 'Z' || ch4 < 'A' || ch4 > 'Z') {
throw new IllegalArgumentException("Invalid locale format: " + str);
}
if (len == 5) {
return new Locale(str.substring(0, 2), str.substring(3, 5));
} else {
if (str.charAt(5) != '_') {
throw new IllegalArgumentException("Invalid locale format: " + str);
}
return new Locale(str.substring(0, 2), str.substring(3, 5), str.substring(6));
}
}
}
/**
* Factory method for wrapping a JCR node in a JcrTaxonomy object. Override to customize
* the taxonomy repository structure.
*/
protected JcrTaxonomy newTaxonomy(IModel<Node> model, boolean editing) {
return new JcrTaxonomy(model, editing);
}
@Override
protected void redraw() {
AjaxRequestTarget target = AjaxRequestTarget.get();
if (target != null) {
target.addComponent(holder);
} else {
super.redraw();
}
}
@Override
public void render(PluginRequestTarget target) {
tree.updateTree(target);
super.render(target);
}
@Override
public void onModelChanged() {
redraw();
}
/**
* Returns the current editable category instance
* which is being edited.
*
* @return
*/
protected EditableCategory getCategory() {
return taxonomy.getCategoryByKey(key);
}
TreeNode getSelectedNode() {
Collection<Object> selected = tree.getTreeState().getSelectedNodes();
if (selected.size() == 0) {
return null;
}
return (TreeNode) selected.iterator().next();
}
private List<IModel<String>> getSynonymList() {
String[] synonyms = synonymModel.getObject();
List<IModel<String>> list = new ArrayList<IModel<String>>(synonyms.length);
for (int i = 0; i < synonyms.length; i++) {
final int j = i;
list.add(new IModel<String>() {
private static final long serialVersionUID = 1L;
public String getObject() {
String[] synonyms = synonymModel.getObject();
return synonyms[j];
}
public void setObject(String object) {
String[] synonyms = synonymModel.getObject();
synonyms[j] = object;
synonymModel.setObject(synonyms);
}
public void detach() {
}
});
}
return list;
}
private List<LanguageSelection> getAvailableLanguageSelections() {
List<LanguageSelection> languageSelections = new ArrayList<LanguageSelection>();
for (String locale : taxonomy.getLocales()) {
try {
Locale localeObj = toLocale(locale);
languageSelections.add(new LanguageSelection(localeObj, getLocale()));
} catch (Exception e) {
LOG.warn("Invalid locale for the taxonomy: {}", locale);
}
}
LanguageSelection defaultLanguageSelection = new LanguageSelection(getLocale(), getLocale());
if (!languageSelections.contains(defaultLanguageSelection)) {
languageSelections.add(0, defaultLanguageSelection);
}
return languageSelections;
}
public LanguageSelection getCurrentLanguageSelection() {
return currentLanguageSelection;
}
public void setCurrentLanguageSelection(LanguageSelection currentLanguageSelection) {
this.currentLanguageSelection = currentLanguageSelection;
}
/**
* Returns the detail form container which holds all the category detail fields such as name, description and
* synonyms.
* <p/>
* If you want to add custom UI components for your custom category fields, you might want to override this plugin
* and invoke this method in the constructor to add the custom UI components.
* </P>
*
* @return
*/
protected Form<?> getContainerForm() {
return container;
}
private final class DescriptionModel implements IModel<String> {
private static final long serialVersionUID = 1L;
@Override
public String getObject() {
EditableCategory category = getCategory();
if (category != null) {
return category.getInfo(currentLanguageSelection.getLanguageCode()).getDescription();
}
return null;
}
@Override
public void setObject(String object) {
EditableCategoryInfo info = getCategory().getInfo(currentLanguageSelection.getLanguageCode());
try {
info.setDescription(object);
} catch (TaxonomyException e) {
error(e.getMessage());
redraw();
}
}
public void detach() {
}
}
private final class NameModel implements IModel<String> {
private static final long serialVersionUID = 1L;
@Override
public String getObject() {
EditableCategory category = getCategory();
if (category != null) {
return category.getInfo(currentLanguageSelection.getLanguageCode()).getName();
}
return null;
}
@Override
public void setObject(String object) {
EditableCategory category = taxonomy.getCategoryByKey(key);
EditableCategoryInfo info = category.getInfo(currentLanguageSelection.getLanguageCode());
try {
info.setName(object);
} catch (TaxonomyException e) {
error(e.getMessage());
redraw();
}
}
@Override
public void detach() {
}
}
private final class KeyModel implements IModel<String> {
private static final long serialVersionUID = 1L;
@Override
public String getObject() {
return key;
}
@Override
public void setObject(String object) {
// do nothing
}
public void detach() {
}
}
protected final class LanguageSelection implements Serializable {
private static final long serialVersionUID = 1L;
private String languageCode;
private String displayName;
/**
* Constructor
*
* @param selectionLocale the locale for the actual language selection item
* @param uiLocale the locale by which the language name is determined
*/
public LanguageSelection(Locale selectionLocale, Locale uiLocale) {
this(selectionLocale.getLanguage(), selectionLocale.getDisplayLanguage(uiLocale));
}
public LanguageSelection(String languageCode, String displayName) {
this.languageCode = languageCode;
this.displayName = displayName;
}
public String getLanguageCode() {
return languageCode;
}
public void setLanguageCode(String languageCode) {
this.languageCode = languageCode;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (!(o instanceof LanguageSelection)) {
return false;
}
if (languageCode == null && ((LanguageSelection) o).getLanguageCode() == null) {
return true;
}
if (languageCode != null && languageCode.equals(((LanguageSelection) o).getLanguageCode())) {
return true;
}
return false;
}
@Override
public int hashCode() {
if (languageCode != null) {
return languageCode.hashCode();
}
return super.hashCode();
}
@Override
public String toString() {
return super.toString() + " [ " + languageCode + ", " + displayName + " }";
}
}
}