/* (c) 2016 - 2017 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wms.web.data;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import org.apache.commons.io.IOUtils;
import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.Session;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.attributes.AjaxCallListener;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
import org.apache.wicket.ajax.markup.html.form.AjaxSubmitLink;
import org.apache.wicket.core.util.string.JavaScriptUtils;
import org.apache.wicket.markup.html.WebMarkupContainer;
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.TextField;
import org.apache.wicket.markup.html.form.upload.FileUpload;
import org.apache.wicket.markup.html.form.upload.FileUploadField;
import org.apache.wicket.markup.html.image.Image;
import org.apache.wicket.model.CompoundPropertyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.resource.AbstractResource;
import org.apache.wicket.validation.IValidatable;
import org.apache.wicket.validation.IValidator;
import org.apache.wicket.validation.ValidationError;
import org.geoserver.catalog.StyleGenerator;
import org.geoserver.catalog.StyleInfo;
import org.geoserver.catalog.StyleType;
import org.geoserver.catalog.Styles;
import org.geoserver.catalog.WorkspaceInfo;
import org.geoserver.catalog.impl.StyleInfoImpl;
import org.geoserver.config.GeoServerDataDirectory;
import org.geoserver.ows.util.ResponseUtils;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.resource.Resource;
import org.geoserver.web.data.workspace.WorkspaceChoiceRenderer;
import org.geoserver.web.data.workspace.WorkspacesModel;
import org.geoserver.web.wicket.GeoServerAjaxFormLink;
import org.geoserver.web.wicket.ParamResourceModel;
import org.geoserver.wms.GetLegendGraphicRequest;
import org.geoserver.wms.legendgraphic.BufferedImageLegendGraphicBuilder;
import org.geoserver.wms.web.publish.StyleChoiceRenderer;
import org.geoserver.wms.web.publish.StyleTypeChoiceRenderer;
import org.geoserver.wms.web.publish.StyleTypeModel;
import org.geoserver.wms.web.publish.StylesModel;
import org.geotools.styling.Style;
import org.geotools.util.logging.Logging;
/**
* Style page tab for displaying editing the style metadata.
* Includes style upload and generation functionality.
* Delegates to {@link ExternalGraphicPanel} for editing the legend.
*/
public class StyleAdminPanel extends StyleEditTabPanel {
private static final long serialVersionUID = -2443344473474977026L;
private static final Logger LOGGER = Logging.getLogger(StyleAdminPanel.class);
protected TextField<String> nameTextField;
protected DropDownChoice<WorkspaceInfo> wsChoice;
protected DropDownChoice<String> formatChoice;
protected MarkupContainer formatReadOnlyMessage;
protected WebMarkupContainer legendContainer;
protected ExternalGraphicPanel legendPanel;
protected Image legendImg;
protected DropDownChoice<StyleType> templates;
protected AjaxSubmitLink generateLink;
protected DropDownChoice<StyleInfo> styles;
protected AjaxSubmitLink copyLink;
protected FileUploadField fileUploadField;
protected AjaxSubmitLink uploadLink;
transient BufferedImage legendImage;
public StyleAdminPanel(String id, AbstractStylePage parent) {
super(id, parent);
initUI(parent.getStyleModel());
if (stylePage instanceof StyleEditPage) {
//global styles only editable by full admin
if (!stylePage.isAuthenticatedAsAdmin() && parent.getStyleInfo().getWorkspace() == null) {
nameTextField.setEnabled(false);
uploadLink.setEnabled(false);
}
if (StylePage.isDefaultStyle(getStylePage().getStyleInfo())) {
nameTextField.setEnabled(false);
}
// format only settable upon creation
formatChoice.setEnabled(false);
formatReadOnlyMessage.setVisible(true);
}
}
public void initUI(CompoundPropertyModel<StyleInfo> styleModel) {
StyleInfo style = getStylePage().getStyleInfo();
IModel<String> nameBinding = styleModel.bind("name");
add(nameTextField = new TextField<String>("name", nameBinding));
nameTextField.setRequired(true);
//when editing a default style, disallow changing the name
if (StylePage.isDefaultStyle(style)) {
nameTextField.add(new IValidator<String>() {
String originalName = style.getName();
@Override public void validate(IValidatable<String> validatable) {
if (originalName != null && !originalName.equals(validatable.getValue())) {
ValidationError error = new ValidationError();
error.setMessage( "Can't change the name of default styles." );
error.addKey("editDefaultStyleNameDisallowed");
validatable.error(error);
}
}
});
}
IModel<WorkspaceInfo> wsBinding = styleModel.bind("workspace");
wsChoice =
new DropDownChoice<WorkspaceInfo>("workspace", wsBinding, new WorkspacesModel(), new WorkspaceChoiceRenderer());
wsChoice.setNullValid(true);
if (!stylePage.isAuthenticatedAsAdmin()) {
wsChoice.setNullValid(false);
wsChoice.setRequired(true);
}
add(wsChoice);
//always disable the workspace toggle if not admin
if (!stylePage.isAuthenticatedAsAdmin()) {
wsChoice.setEnabled(false);
}
IModel<String> formatBinding = styleModel.bind("format");
formatChoice = new DropDownChoice<String>("format", formatBinding, new StyleFormatsModel(),
new ChoiceRenderer<String>() {
private static final long serialVersionUID = 2064887235303504013L;
@Override
public String getIdValue(String object, int index) {
return object;
}
@Override
public Object getDisplayValue(String object) {
return Styles.handler(object).getName();
}
});
formatChoice.add(new AjaxFormComponentUpdatingBehavior("change") {
private static final long serialVersionUID = -8372146231225388561L;
@Override
protected void onUpdate(AjaxRequestTarget target) {
target.appendJavaScript(String.format(
"if (document.gsEditors) { document.gsEditors.editor.setOption('mode', '%s'); }", stylePage.styleHandler().getCodeMirrorEditMode()));
}
});
add(formatChoice);
formatReadOnlyMessage = new WebMarkupContainer("formatReadOnly", new Model<String>());
formatReadOnlyMessage.setVisible(false);
add(formatReadOnlyMessage);
// add the Legend fields
legendPanel = new ExternalGraphicPanel("legendPanel", styleModel, stylePage.styleForm);
legendPanel.setOutputMarkupId(true);
add(legendPanel);
if (style.getId() != null) {
try {
stylePage.setRawStyle(stylePage.readFile(style));
} catch (IOException e) {
// ouch, the style file is gone! Register a generic error message
Session.get().error(new ParamResourceModel("styleNotFound", this, style.getFilename()).getString());
}
}
// style generation functionality
templates = new DropDownChoice<StyleType>("templates", new Model<StyleType>(), new StyleTypeModel(), new StyleTypeChoiceRenderer());
templates.setOutputMarkupId(true);
templates.add(new AjaxFormComponentUpdatingBehavior("change") {
private static final long serialVersionUID = -6649152103570059645L;
@Override
protected void onUpdate(AjaxRequestTarget target) {
templates.validate();
generateLink.setEnabled(templates.getConvertedInput() != null);
target.add(generateLink);
}
});
add(templates);
generateLink = generateLink();
generateLink.setEnabled(false);
add(generateLink);
// style copy functionality
styles = new DropDownChoice<StyleInfo>("existingStyles", new Model<StyleInfo>(), new StylesModel(), new StyleChoiceRenderer());
styles.setOutputMarkupId(true);
styles.add(new AjaxFormComponentUpdatingBehavior("change") {
private static final long serialVersionUID = 8098121930876372129L;
@Override
protected void onUpdate(AjaxRequestTarget target) {
styles.validate();
copyLink.setEnabled(styles.getConvertedInput() != null);
target.add(copyLink);
}
});
add(styles);
copyLink = copyLink();
copyLink.setEnabled(false);
add(copyLink);
uploadLink = uploadLink();
//uploadLink.setEnabled(false);
add(uploadLink);
fileUploadField = new FileUploadField("filename");
//Explicitly set model so this doesn't use the form model
fileUploadField.setDefaultModel(new Model<String>(""));
add(fileUploadField);
add(previewLink());
legendContainer = new WebMarkupContainer("legendContainer");
legendContainer.setOutputMarkupId(true);
add(legendContainer);
this.legendImg = new Image("legendImg", new AbstractResource() {
private static final long serialVersionUID = -6932528694575832606L;
@Override
protected ResourceResponse newResourceResponse(Attributes attributes) {
ResourceResponse rr = new ResourceResponse();
rr.setContentType("image/png");
rr.setWriteCallback(new WriteCallback() {
@Override
public void writeData(Attributes attributes) throws IOException {
ImageIO.write(legendImage, "PNG", attributes.getResponse().getOutputStream());
}
});
return rr;
}});
legendContainer.add(this.legendImg);
this.legendImg.setVisible(false);
this.legendImg.setOutputMarkupId(true);
}
/**
* Clears validation messages from form input elements.
* Called when it is necessary to submit the form without needing to show validation,
* such as when you are generating a new style
*/
protected void clearFeedbackMessages() {
nameTextField.getFeedbackMessages().clear();
wsChoice.getFeedbackMessages().clear();
formatChoice.getFeedbackMessages().clear();
stylePage.editor.getFeedbackMessages().clear();
}
protected Component previewLink() {
return new GeoServerAjaxFormLink("preview", stylePage.styleForm) {
private static final long serialVersionUID = 7404304424029960594L;
@Override
protected void onClick(AjaxRequestTarget target, Form<?> form) {
stylePage.editor.processInput();
wsChoice.processInput();
clearFeedbackMessages();
legendImg.setVisible(false);
// Generate the legend
//Try External Legend
URLConnection conn = legendPanel.getExternalGraphic(target, form);
String onlineResource = legendPanel.getOnlineResource();
if (onlineResource != null && !onlineResource.isEmpty()) {
if (conn != null) {
try {
legendImage = ImageIO.read(conn.getInputStream());
legendImg.setVisible(true);
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to render external legend graphic", e);
legendContainer.error("Failed to render external legend graphic");
}
}
} else {
//No external legend, use generated legend
GeoServerDataDirectory dd = GeoServerExtensions.bean(GeoServerDataDirectory.class, stylePage.getGeoServerApplication().getApplicationContext());
StyleInfo si = new StyleInfoImpl(stylePage.getCatalog());
si.setFormat(stylePage.getStyleInfo().getFormat());
String styleName = "tmp" + UUID.randomUUID().toString();
String styleFileName = styleName + '.' + Styles.handler(si.getFormat()).getFileExtension();
si.setFilename(styleFileName);
si.setName(styleName);
si.setWorkspace(stylePage.styleModel.getObject().getWorkspace());
Resource styleResource = null;
try {
styleResource = dd.style(si);
try(OutputStream os = styleResource.out()) {
IOUtils.write(stylePage.editor.getInput(), os);
}
Style style = dd.parsedStyle(si);
if (style != null) {
GetLegendGraphicRequest request = new GetLegendGraphicRequest();
request.setLayer(null);
request.setStyle(style);
request.setStrict(false);
Map<String, String> legendOptions = new HashMap<String, String>();
legendOptions.put("forceLabels", "on");
legendOptions.put("fontAntiAliasing", "true");
request.setLegendOptions(legendOptions);
BufferedImageLegendGraphicBuilder builder = new BufferedImageLegendGraphicBuilder();
legendImage = builder.buildLegendGraphic(request);
legendImg.setVisible(true);
}
} catch (IOException e) {
throw new WicketRuntimeException(e);
} catch (Exception e) {
legendImg.setVisible(false);
legendContainer.error("Failed to build legend preview. Check to see if the style is valid.");
LOGGER.log(Level.WARNING, "Failed to build legend preview", e);
} finally {
if(styleResource != null) {
styleResource.delete();
}
}
}
target.add(legendContainer);
}
@Override
protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
super.updateAjaxAttributes(attributes);
attributes.getAjaxCallListeners().add(stylePage.editor.getSaveDecorator());
}
};
}
protected AjaxSubmitLink generateLink() {
return new ConfirmOverwriteSubmitLink("generate") {
private static final long serialVersionUID = 55921414750155395L;
@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
// we need to force validation or the value won't be converted
templates.processInput();
nameTextField.processInput();
wsChoice.processInput();
StyleType template = (StyleType) templates.getConvertedInput();
StyleGenerator styleGen = new StyleGenerator(stylePage.getCatalog());
styleGen.setWorkspace(getStylePage().getStyleInfo().getWorkspace());
if (template != null) {
try {
// same here, force validation or the field won't be updated
stylePage.editor.reset();
stylePage.setRawStyle(new StringReader(styleGen.generateStyle(
stylePage.styleHandler(), template, getStylePage().getStyleInfo().getName())));
target.appendJavaScript(String.format(
"if (document.gsEditors) { document.gsEditors.editor.setOption('mode', '%s'); }",
stylePage.styleHandler().getCodeMirrorEditMode()));
clearFeedbackMessages();
} catch (Exception e) {
clearFeedbackMessages();
stylePage.editor.getFeedbackMessages().clear();
stylePage.error("Errors occurred generating the style");
LOGGER.log(Level.WARNING, "Errors occured generating the style", e);
}
target.add(stylePage);
}
}
};
}
protected AjaxSubmitLink copyLink() {
return new ConfirmOverwriteSubmitLink("copy") {
private static final long serialVersionUID = -6388040033082157163L;
@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
// we need to force validation or the value won't be converted
styles.processInput();
StyleInfo style = (StyleInfo) styles.getConvertedInput();
if (style != null) {
try {
// same here, force validation or the field won't be updated
stylePage.editor.reset();
stylePage.setRawStyle(stylePage.readFile(style));
target.appendJavaScript(String.format(
"if (document.gsEditors) { document.gsEditors.editor.setOption('mode', '%s'); }",
stylePage.styleHandler().getCodeMirrorEditMode()));
clearFeedbackMessages();
} catch (Exception e) {
clearFeedbackMessages();
stylePage.error("Errors occurred loading the '" + style.getName() + "' style");
LOGGER.log(Level.WARNING, "Errors occurred loading the '" + style.getName() + "' style", e);
}
target.add(stylePage);
}
}
};
}
AjaxSubmitLink uploadLink() {
return new ConfirmOverwriteSubmitLink("upload", stylePage.styleForm) {
private static final long serialVersionUID = 658341311654601761L;
@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
FileUpload upload = fileUploadField.getFileUpload();
if (upload == null) {
warn("No file selected.");
return;
}
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try {
IOUtils.copy(upload.getInputStream(), bout);
stylePage.editor.reset();
stylePage.setRawStyle(new InputStreamReader(new ByteArrayInputStream(bout.toByteArray()), "UTF-8"));
target.appendJavaScript(String.format(
"if (document.gsEditors) { document.gsEditors.editor.setOption('mode', '%s'); }",
stylePage.styleHandler().getCodeMirrorEditMode()));
clearFeedbackMessages();
} catch (IOException e) {
throw new WicketRuntimeException(e);
} catch (Exception e) {
clearFeedbackMessages();
stylePage.error("Errors occurred uploading the '" + upload.getClientFileName() + "' style");
LOGGER.log(Level.WARNING, "Errors occurred uploading the '" + upload.getClientFileName() + "' style", e);
}
// update the style object
StyleInfo s = getStylePage().getStyleInfo();
if (s.getName() == null || "".equals(s.getName().trim())) {
// set it
nameTextField.setModelValue(new String[] {ResponseUtils.stripExtension(upload
.getClientFileName())});
nameTextField.modelChanged();
}
target.add(stylePage);
}
};
}
class ConfirmOverwriteSubmitLink extends AjaxSubmitLink {
private static final long serialVersionUID = 2673499149884774636L;
public ConfirmOverwriteSubmitLink(String id) {
super(id);
}
public ConfirmOverwriteSubmitLink(String id, Form<?> form) {
super(id, form);
}
@Override
protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
super.updateAjaxAttributes(attributes);
attributes.getAjaxCallListeners().add(new AjaxCallListener() {
/** serialVersionUID */
private static final long serialVersionUID = 8637613472102572505L;
@Override
public CharSequence getPrecondition(Component component) {
CharSequence message = new ParamResourceModel("confirmOverwrite", stylePage)
.getString();
message = JavaScriptUtils.escapeQuotes(message);
return "var val = attrs.event.view.document.gsEditors ? "
+ "attrs.event.view.document.gsEditors." + stylePage.editor.getTextAreaMarkupId() + ".getValue() : "
+ "attrs.event.view.document.getElementById(\"" + stylePage.editor.getTextAreaMarkupId() + "\").value; "
+ "if(val != '' &&"
+ "!confirm('"
+ message + "')) return false;";
}
});
}
@Override
public boolean getDefaultFormProcessing() {
return false;
}
}
}