/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wms.web.data;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Level;
import org.apache.wicket.Component;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
import org.apache.wicket.ajax.markup.html.form.AjaxSubmitLink;
import org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow;
import org.apache.wicket.extensions.ajax.markup.html.tabs.AjaxTabbedPanel;
import org.apache.wicket.extensions.markup.html.tabs.AbstractTab;
import org.apache.wicket.extensions.markup.html.tabs.ITab;
import org.apache.wicket.extensions.markup.html.tabs.PanelCachingTab;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.CompoundPropertyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.DataStoreInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.catalog.ResourcePool;
import org.geoserver.catalog.StyleHandler;
import org.geoserver.catalog.StyleInfo;
import org.geoserver.catalog.Styles;
import org.geoserver.catalog.WorkspaceInfo;
import org.geoserver.catalog.impl.LayerInfoImpl;
import org.geoserver.config.GeoServerDataDirectory;
import org.geoserver.platform.resource.Resource;
import org.geoserver.web.ComponentAuthorizer;
import org.geoserver.web.GeoServerApplication;
import org.geoserver.web.GeoServerSecuredPage;
import org.geoserver.web.wicket.CodeMirrorEditor;
import org.geoserver.web.wicket.GeoServerAjaxFormLink;
import org.geoserver.web.wicket.ParamResourceModel;
import org.xml.sax.SAXParseException;
/**
* Base page for creating/editing styles
* <p>
* WARNING: one crucial aspect of this page is its ability to not loose edits when one switches from
* one tab to the other. I did not find any effective way to unit test this, so _please_, if you do
* modify anything in this class (especially the models), manually retest that the edits are not
* lost on tab switch.
*/
@SuppressWarnings("serial")
public abstract class AbstractStylePage extends GeoServerSecuredPage {
protected Form<StyleInfo> styleForm;
protected AjaxTabbedPanel<ITab> tabbedPanel;
protected CodeMirrorEditor editor;
protected ModalWindow popup;
protected CompoundPropertyModel<StyleInfo> styleModel;
protected IModel<LayerInfo> layerModel;
String rawStyle;
public AbstractStylePage() {
}
public AbstractStylePage(StyleInfo style) {
recoverCssStyle(style);
initPreviewLayer(style);
initUI(style);
}
protected void initPreviewLayer(StyleInfo style) {
Catalog catalog = getCatalog();
List<LayerInfo> layers;
//Try getting the first layer associated with this style
if (style != null) {
layers = catalog.getLayers(style);
if (layers.size() > 0) {
layerModel = new Model<LayerInfo>(layers.get(0));
return;
}
}
//Try getting the first layer in the default store in the default workspace
WorkspaceInfo defaultWs = catalog.getDefaultWorkspace();
if (defaultWs != null) {
DataStoreInfo defaultStore = catalog.getDefaultDataStore(defaultWs);
if (defaultStore != null) {
List<ResourceInfo> resources = catalog.getResourcesByStore(defaultStore, ResourceInfo.class);
for (ResourceInfo resource : resources) {
layers = catalog.getLayers(resource);
if (layers.size() > 0) {
layerModel = new Model<LayerInfo>(layers.get(0));
return;
}
}
}
}
//Try getting the first layer returned by the catalog
layers = catalog.getLayers();
if (layers.size() > 0) {
layerModel = new Model<LayerInfo>(layers.get(0));
return;
}
//If none of these succeeded, return an empty model
layerModel = new Model<LayerInfo>(new LayerInfoImpl());
}
protected void initUI(StyleInfo style) {
/* init model */
if (style == null) {
styleModel = new CompoundPropertyModel<StyleInfo>(getCatalog().getFactory().createStyle());
styleModel.getObject().setName("");
styleModel.getObject().setLegend(getCatalog().getFactory().createLegend());
} else {
if (style.getLegend() == null) {
style.setLegend(getCatalog().getFactory().createLegend());
}
styleModel = new CompoundPropertyModel<StyleInfo>(style);
}
/* init main form */
styleForm = new Form<StyleInfo>("styleForm", styleModel) {
@Override
protected void onSubmit() {
onStyleFormSubmit();
super.onSubmit();
}
};
add(styleForm);
styleForm.setMultiPart(true);
/* init popup */
popup = new ModalWindow("popup");
styleForm.add(popup);
/* init tabs */
List<ITab> tabs = new ArrayList<ITab>();
//Well known tabs
PanelCachingTab dataTab = new PanelCachingTab(new AbstractTab(new Model<String>("Data")) {
public Panel getPanel(String id) {
return new StyleAdminPanel(id, AbstractStylePage.this);
}
});
PanelCachingTab publishingTab = new PanelCachingTab(new AbstractTab(new Model<String>("Publishing")) {
private static final long serialVersionUID = 4184410057835108176L;
public Panel getPanel(String id) {
return new LayerAssociationPanel(id, AbstractStylePage.this);
};
});
PanelCachingTab previewTab = new PanelCachingTab(new AbstractTab(new Model<String>("Layer Preview")) {
public Panel getPanel(String id) {
return new OpenLayersPreviewPanel(id, AbstractStylePage.this);
}
});
PanelCachingTab attributeTab = new PanelCachingTab(new AbstractTab(new Model<String>("Layer Attributes")) {
private static final long serialVersionUID = 4184410057835108176L;
public Panel getPanel(String id) {
try {
return new LayerAttributePanel(id, AbstractStylePage.this);
} catch (IOException e) {
throw new WicketRuntimeException(e);
}
};
});
//If style is null, this is a new style.
//If so, we want to disable certain tabs
tabs.add(dataTab);
if (style != null) {
tabs.add(publishingTab);
tabs.add(previewTab);
tabs.add(attributeTab);
}
//Dynamic tabs
List<StyleEditTabPanelInfo> tabPanels = getGeoServerApplication().getBeansOfType(StyleEditTabPanelInfo.class);
// sort the tabs based on order
Collections.sort(tabPanels, new Comparator<StyleEditTabPanelInfo>() {
public int compare(StyleEditTabPanelInfo o1, StyleEditTabPanelInfo o2) {
Integer order1 = o1.getOrder() >= 0 ? o1.getOrder() : Integer.MAX_VALUE;
Integer order2 = o2.getOrder() >= 0 ? o2.getOrder() : Integer.MAX_VALUE;
return order1.compareTo(order2);
}
});
// instantiate tab panels and add to tabs list
for (StyleEditTabPanelInfo tabPanelInfo : tabPanels) {
String titleKey = tabPanelInfo.getTitleKey();
IModel<String> titleModel = null;
if (tabPanelInfo.isEnabledOnNew() || style != null) {
if (titleKey != null) {
titleModel = new org.apache.wicket.model.ResourceModel(titleKey);
} else {
titleModel = new Model<String>(tabPanelInfo.getComponentClass().getSimpleName());
}
final Class<StyleEditTabPanel> panelClass = tabPanelInfo.getComponentClass();
tabs.add(new AbstractTab(titleModel) {
private static final long serialVersionUID = -6637277497986497791L;
@Override
public Panel getPanel(String panelId) {
StyleEditTabPanel tabPanel;
try {
tabPanel = panelClass.getConstructor(String.class, IModel.class)
.newInstance(panelId, styleModel);
} catch (Exception e) {
throw new WicketRuntimeException(e);
}
return tabPanel;
}
});
}
}
tabbedPanel = new AjaxTabbedPanel<ITab>("context", tabs) {
protected String getTabContainerCssClass()
{
return "tab-row tab-row-compact";
}
@Override
protected WebMarkupContainer newLink(String linkId, final int index) {
/*
* Use a submit link here in order to save the state of the current tab to the model
* setDefaultFormProcessing(false) is used so that we do not do a full submit
* (with validation + saving to the catalog)
*/
AjaxSubmitLink link = new AjaxSubmitLink(linkId) {
private static final long serialVersionUID = 1L;
@Override
public void onSubmit(AjaxRequestTarget target, Form<?> form) {
if (getLayerInfo() == null || getLayerInfo().getId() == null) {
switch (index) {
case 1:
tabbedPanel.error("Cannot show Publishing options: No Layers available.");
target.add(feedbackPanel);
return;
case 2:
tabbedPanel.error("Cannot show Layer Preview: No Layers available.");
target.add(feedbackPanel);
return;
case 3:
tabbedPanel.error("Cannot show Attribute Preview: No Layers available.");
target.add(feedbackPanel);
return;
default:
break;
}
}
setSelectedTab(index);
target.add(tabbedPanel);
}
};
link.setDefaultFormProcessing(false);
return link;
}
};
styleForm.add(tabbedPanel);
/* init editor */
styleForm.add(editor = new CodeMirrorEditor("styleEditor", styleHandler()
.getCodeMirrorEditMode(), new PropertyModel<String>(this, "rawStyle")));
// force the id otherwise this blasted thing won't be usable from other forms
editor.setTextAreaMarkupId("editor");
editor.setOutputMarkupId(true);
editor.setRequired(true);
styleForm.add(editor);
add(validateLink());
add(new AjaxSubmitLink("apply", styleForm) {
@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
//If we have a new style, go to the edit page
if (style == null) {
StyleInfo s = getStyleInfo();
PageParameters parameters = new PageParameters();
parameters.add(StyleEditPage.NAME, s.getName());
if (s.getWorkspace() != null) {
parameters.add(StyleEditPage.WORKSPACE, s.getWorkspace().getName());
}
getRequestCycle().setResponsePage(StyleEditPage.class, parameters);
}
target.add(feedbackPanel);
//Update preview if we are on the preview tab
if (style != null && tabbedPanel.getSelectedTab() == 2) {
tabbedPanel.visitChildren(StyleEditTabPanel.class, (component, visit) -> {
if (component instanceof OpenLayersPreviewPanel) {
OpenLayersPreviewPanel previewPanel = (OpenLayersPreviewPanel) component;
try {
target.appendJavaScript(previewPanel.getUpdateCommand());
} catch (Exception e) {
LOGGER.log(Level.FINER, e.getMessage(), e);
}
}
});
}
}
@Override
protected void onAfterSubmit(AjaxRequestTarget target, Form<?> form) {
// Re-initialize the Legend model object, if it is null.
if (styleModel.getObject().getLegend() == null) {
styleModel.getObject().setLegend(getCatalog().getFactory().createLegend());
}
}
@Override
protected void onError(AjaxRequestTarget target, Form<?> form) {
target.add(feedbackPanel);
}
});
add(new AjaxSubmitLink("submit", styleForm) {
@Override
protected void onAfterSubmit(AjaxRequestTarget target, Form<?> form) {
if (form.hasError()) {
target.add(feedbackPanel);
} else {
doReturn(StylePage.class);
}
}
@Override
protected void onError(AjaxRequestTarget target, Form<?> form) {
target.add(feedbackPanel);
}
});
Link<StylePage> cancelLink = new Link<StylePage>("cancel") {
@Override
public void onClick() {
doReturn(StylePage.class);
}
};
add(cancelLink);
}
StyleHandler styleHandler() {
String format = styleModel.getObject().getFormat();
return Styles.handler(format);
}
Component validateLink() {
return new GeoServerAjaxFormLink("validate", styleForm) {
@Override
protected void onClick(AjaxRequestTarget target, Form<?> form) {
editor.processInput();
List<Exception> errors = validateSLD();
if ( errors.isEmpty() ) {
form.info( "No validation errors.");
} else {
for( Exception e : errors ) {
form.error( sldErrorWithLineNo(e) );
}
}
}
@Override
protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
super.updateAjaxAttributes(attributes);
attributes.getAjaxCallListeners().add(editor.getSaveDecorator());
}
};
}
private String sldErrorWithLineNo(Exception e) {
if (e instanceof SAXParseException) {
SAXParseException se = (SAXParseException) e;
return "line " + se.getLineNumber() + ": " + e.getLocalizedMessage();
}
String message = e.getLocalizedMessage();
if(message != null) {
return message;
} else {
return new ParamResourceModel("genericError", this).getString();
}
}
List<Exception> validateSLD() {
try {
final String sld = editor.getInput();
ByteArrayInputStream input = new ByteArrayInputStream(sld.getBytes());
List<Exception> validationErrors = styleHandler().validate(input, null, getCatalog().getResourcePool().getEntityResolver());
return validationErrors;
} catch( Exception e ) {
return Arrays.asList( e );
}
}
Reader readFile(StyleInfo style) throws IOException {
ResourcePool pool = getCatalog().getResourcePool();
return pool.readStyle(style);
}
public void setRawStyle(Reader in) throws IOException {
BufferedReader bin = null;
if ( in instanceof BufferedReader ) {
bin = (BufferedReader) in;
}
else {
bin = new BufferedReader( in );
}
StringBuilder builder = new StringBuilder();
String line = null;
while ((line = bin.readLine()) != null ) {
builder.append(line).append("\n");
}
this.rawStyle = builder.toString();
editor.setModelObject(rawStyle);
in.close();
}
/**
* Check for an original CSS version of the style created by the old CSS extension (pre-pluggable styles). If a CSS style is found, recover it if
* the derived SLD has not subsequently been manually edited.
*
* The recovery is accomplished by updating the catalog to point to the original CSS file, and changing the style's format to "css".
*
* @param si The {@link StyleInfo} for which to check for and potentially recover a CSS version.
*/
protected void recoverCssStyle(StyleInfo si) {
if (si == null) {
return;
}
// Only try to repair missing CSS files if a CSS style handler is registered.
try {
Styles.handler("css");
} catch (Exception e) {
return;
}
// Use this tolerance to prevent erasing an SLD that was manually edited after being
// generated from a CSS file. (Generated SLDs will always be newer than the CSS).
long favorSLDIfNewerByMS = 600000;
// The problem only exists for styles with an "sld" format (either explicitly or by default).
if ("sld".equalsIgnoreCase(si.getFormat())) {
String filename = si.getFilename();
String filenameCss = filename.substring(0, filename.lastIndexOf('.')) + ".css";
GeoServerDataDirectory dataDir = new GeoServerDataDirectory(
getCatalog().getResourceLoader());
Resource cssResource = dataDir.get(si, filenameCss);
if (!cssResource.getType().equals(Resource.Type.UNDEFINED)) {
// If there is an existing CSS file with the style's name, check if it should be recovered.
Resource sldResource = dataDir.get(si, filename);
long sldNewerByMs = sldResource.lastmodified() - cssResource.lastmodified();
if (sldNewerByMs > favorSLDIfNewerByMS) {
LOGGER.log(Level.WARNING,
"A CSS version of " + si.getName() + " has been recovered ("
+ filenameCss + "), but the SLD is more recent by "
+ sldNewerByMs + " ms. The style will be left as an SLD.");
} else {
LOGGER.log(Level.WARNING,
"A CSS version of " + si.getName() + " has been recovered ("
+ filenameCss + "). The style will be converted to CSS.");
si.setFilename(filenameCss);
si.setFormat("css");
getCatalog().save(si);
}
}
}
}
/**
* Called when a configuration change requires updating an inactive tab
*/
protected void configurationChanged() {
tabbedPanel.visitChildren(StyleEditTabPanel.class, (component, visit) -> {
if (component instanceof StyleEditTabPanel) {
((StyleEditTabPanel) component).configurationChanged();
}
});
}
/**
* Subclasses must implement to define the submit behavior
*/
protected abstract void onStyleFormSubmit();
protected ModalWindow getPopup() {
return popup;
}
protected IModel<LayerInfo> getLayerModel() {
return layerModel;
}
protected CompoundPropertyModel<StyleInfo> getStyleModel() {
return styleModel;
}
public LayerInfo getLayerInfo() {
return layerModel.getObject();
}
public StyleInfo getStyleInfo() {
return styleModel.getObject();
}
@Override
protected ComponentAuthorizer getPageAuthorizer() {
return ComponentAuthorizer.WORKSPACE_ADMIN;
}
//Make sure child tabs can see this
@Override
protected boolean isAuthenticatedAsAdmin() {
return super.isAuthenticatedAsAdmin();
}
@Override
protected Catalog getCatalog() {
return super.getCatalog();
}
@Override
protected GeoServerApplication getGeoServerApplication() {
return super.getGeoServerApplication();
}
}