/* (c) 2014 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.web.data.layer;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.apache.wicket.Component;
import org.apache.wicket.PageParameters;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.html.form.CheckBox;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.IChoiceRenderer;
import org.apache.wicket.markup.html.form.SubmitLink;
import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.model.CompoundPropertyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.validation.IValidatable;
import org.apache.wicket.validation.validator.AbstractValidator;
import org.geoserver.catalog.DataStoreInfo;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.web.ComponentAuthorizer;
import org.geoserver.web.GeoServerSecuredPage;
import org.geoserver.web.data.store.StorePage;
import org.geoserver.web.wicket.GeoServerAjaxFormLink;
import org.geoserver.web.wicket.GeoServerTablePanel;
import org.geoserver.web.wicket.ParamResourceModel;
import org.geoserver.web.wicket.GeoServerDataProvider.Property;
import org.geotools.data.DataAccess;
import org.geotools.data.Query;
import org.geotools.data.Transaction;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.jdbc.JDBCDataStore;
import org.geotools.jdbc.VirtualTable;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
/**
* Base page for SQL view creation/editing
*
* @author Andrea Aime - OpenGeo
*/
@SuppressWarnings("serial")
public abstract class SQLViewAbstractPage extends GeoServerSecuredPage {
public static final String DATASTORE = "storeName";
public static final String WORKSPACE = "wsName";
String storeId;
String typeInfoId;
String sql;
String name;
boolean newView;
SQLViewAttributeProvider attProvider;
private TextArea sqlEditor;
private GeoServerTablePanel<SQLViewAttribute> attributes;
private GeoServerTablePanel<Parameter> parameters;
private SQLViewParamProvider paramProvider;
boolean guessGeometrySrid = false;
private CheckBox guessCheckbox;
private boolean escapeSql = true;
private static final List GEOMETRY_TYPES = Arrays.asList(Geometry.class,
GeometryCollection.class, Point.class, MultiPoint.class, LineString.class,
MultiLineString.class, Polygon.class, MultiPolygon.class);
public SQLViewAbstractPage(PageParameters params) throws IOException {
this(params.getString(WORKSPACE), params.getString(DATASTORE), null, null);
}
@SuppressWarnings("deprecation")
public SQLViewAbstractPage(String workspaceName, String storeName, String typeName, VirtualTable virtualTable)
throws IOException {
storeId = getCatalog().getStoreByName(workspaceName, storeName, DataStoreInfo.class)
.getId();
// build the form and the text area
Form form = new Form("form", new CompoundPropertyModel(this));
add(form);
final TextField nameField = new TextField("name");
nameField.setRequired(true);
nameField.add(new ViewNameValidator());
form.add(nameField);
sqlEditor = new TextArea("sql");
form.add(sqlEditor);
// the parameters and attributes provider
attProvider = new SQLViewAttributeProvider();
paramProvider = new SQLViewParamProvider();
// setting up the providers
if (typeName != null) {
newView = false;
// grab the virtual table
DataStoreInfo store = getCatalog().getStore(storeId, DataStoreInfo.class);
FeatureTypeInfo fti = getCatalog().getResourceByStore(store, typeName, FeatureTypeInfo.class);
// the type can be still not saved
if(fti != null) {
typeInfoId = fti.getId();
}
if(virtualTable == null) {
throw new IllegalArgumentException("The specified feature type does not have a sql view attached to it");
}
// get the store
DataAccess da = store.getDataStore(null);
if (!(da instanceof JDBCDataStore)) {
error("Cannot create a SQL view if the store is not database based");
doReturn(StorePage.class);
return;
}
name = virtualTable.getName();
sql = virtualTable.getSql();
escapeSql = virtualTable.isEscapeSql();
paramProvider.init(virtualTable);
try {
SimpleFeatureType ft = testViewDefinition(virtualTable, false);
attProvider.setFeatureType(ft, virtualTable);
} catch(Exception e) {
LOGGER.log(Level.SEVERE, "Failed to build feature type for the sql view", e);
}
} else {
newView = true;
}
// the links to refresh, add and remove a parameter
form.add(new GeoServerAjaxFormLink("guessParams") {
@Override
protected void onClick(AjaxRequestTarget target, Form form) {
sqlEditor.processInput();
parameters.processInputs();
if (sql != null && !"".equals(sql.trim())) {
paramProvider.refreshFromSql(sql);
target.addComponent(parameters);
}
}
});
form.add(new GeoServerAjaxFormLink("addNewParam") {
@Override
protected void onClick(AjaxRequestTarget target, Form form) {
paramProvider.addParameter();
target.addComponent(parameters);
}
});
form.add(new GeoServerAjaxFormLink("removeParam") {
@Override
protected void onClick(AjaxRequestTarget target, Form form) {
paramProvider.removeAll(parameters.getSelection());
parameters.clearSelection();
target.addComponent(parameters);
}
});
// the parameters table
parameters = new GeoServerTablePanel<Parameter>("parameters", paramProvider, true) {
@Override
protected Component getComponentForProperty(String id, IModel itemModel,
Property<Parameter> property) {
Fragment f = new Fragment(id, "text", SQLViewAbstractPage.this);
TextField text = new TextField("text", property.getModel(itemModel));
text.setLabel(new ParamResourceModel("th." + property.getName(), SQLViewAbstractPage.this));
if(property == SQLViewParamProvider.NAME) {
text.setRequired(true);
} else if(property == SQLViewParamProvider.REGEXP) {
text.add(new RegexpValidator());
}
f.add(text);
return f;
}
};
parameters.setFilterVisible(false);
parameters.setSortable(false);
parameters.getTopPager().setVisible(false);
parameters.getBottomPager().setVisible(false);
parameters.setOutputMarkupId(true);
form.add(parameters);
// the "refresh attributes" link
form.add(refreshLink());
form.add(guessCheckbox = new CheckBox("guessGeometrySrid", new PropertyModel(this, "guessGeometrySrid")));
form.add(new CheckBox("escapeSql"));
// the editable attribute table
attributes = new GeoServerTablePanel<SQLViewAttribute>("attributes", attProvider) {
@Override
protected Component getComponentForProperty(String id, IModel itemModel,
Property<SQLViewAttribute> property) {
SQLViewAttribute att = (SQLViewAttribute) itemModel.getObject();
boolean isGeometry = att.getType() != null
&& Geometry.class.isAssignableFrom(att.getType());
if (property == SQLViewAttributeProvider.PK) {
// editor for pk status
Fragment f = new Fragment(id, "checkbox", SQLViewAbstractPage.this);
f.add(new CheckBox("identifier", new PropertyModel(itemModel, "pk")));
return f;
} else if (property == SQLViewAttributeProvider.TYPE && isGeometry) {
Fragment f = new Fragment(id, "geometry", SQLViewAbstractPage.this);
f.add(new DropDownChoice("geometry", new PropertyModel(itemModel, "type"),
GEOMETRY_TYPES, new GeometryTypeRenderer()));
return f;
} else if(property == SQLViewAttributeProvider.SRID && isGeometry) {
Fragment f = new Fragment(id, "text", SQLViewAbstractPage.this);
f.add(new TextField("text", new PropertyModel(itemModel, "srid")));
return f;
}
return null;
}
};
// just a plain table, no filters, no paging,
attributes.setFilterVisible(false);
attributes.setSortable(false);
attributes.setPageable(false);
attributes.setOutputMarkupId(true);
form.add(attributes);
// save and cancel at the bottom of the page
form.add(new SubmitLink("save") {
@Override
public void onSubmit() {
onSave();
}
});
form.add(new Link("cancel") {
@Override
public void onClick() {
onCancel();
}
});
}
private GeoServerAjaxFormLink refreshLink() {
return new GeoServerAjaxFormLink("refresh") {
@Override
protected void onClick(AjaxRequestTarget target, Form form) {
sqlEditor.processInput();
parameters.processInputs();
guessCheckbox.processInput();
if (sql != null && !"".equals(sql.trim())) {
SimpleFeatureType newSchema = null;
try {
newSchema = testViewDefinition(guessGeometrySrid);
if (newSchema != null) {
attProvider.setFeatureType(newSchema, null);
target.addComponent(attributes);
}
} catch (IOException e) {
LOGGER.log(Level.INFO, "Error testing SQL query", e);
error(getFirstErrorMessage(e));
}
}
}
};
}
/**
* Checks the view definition works as expected and returns the feature type guessed solely by
* looking at the sql and the first row of its output
*
* @param newSchema
* @return
* @throws IOException
*/
protected SimpleFeatureType testViewDefinition(boolean guessGeometrySrid) throws IOException {
// check out if the view can be used
JDBCDataStore ds = (JDBCDataStore) getCatalog().getDataStore(storeId).getDataStore(null);
String vtName = null;
try {
// use a highly random name
do {
vtName = UUID.randomUUID().toString();
} while (Arrays.asList(ds.getTypeNames()).contains(vtName));
// try adding the vt and see if that works
VirtualTable vt = new VirtualTable(vtName, sql);
paramProvider.updateVirtualTable(vt);
ds.addVirtualTable(vt);
return guessFeatureType(ds, vt.getName(), guessGeometrySrid);
} finally {
if(vtName != null) {
ds.removeVirtualTable(vtName);
}
}
}
protected SimpleFeatureType getFeatureType(VirtualTable vt) throws IOException {
// check out if the view can be used
JDBCDataStore ds = (JDBCDataStore) getCatalog().getDataStore(storeId).getDataStore(null);
String vtName = null;
try {
// use a highly random name
do {
vtName = UUID.randomUUID().toString();
} while (Arrays.asList(ds.getTypeNames()).contains(vtName));
// try adding the vt and see if that works
ds.addVirtualTable(new VirtualTable(vtName, vt));
return ds.getSchema(vtName);
} finally {
if(vtName != null) {
ds.removeVirtualTable(vtName);
}
}
}
/**
* Checks the view definition works as expected and returns the feature type guessed solely by
* looking at the sql and the first row of its output
*
* @param newSchema
* @return
* @throws IOException
*/
protected SimpleFeatureType testViewDefinition(VirtualTable virtualTable, boolean guessGeometrySrid) throws IOException {
// check out if the view can be used
JDBCDataStore ds = (JDBCDataStore) getCatalog().getDataStore(storeId).getDataStore(null);
String vtName = null;
try {
// use a highly random name
do {
vtName = UUID.randomUUID().toString();
} while (Arrays.asList(ds.getTypeNames()).contains(vtName));
// try adding the vt and see if that works
VirtualTable vt = new VirtualTable(vtName, virtualTable);
// hide the primary key definitions or we'll loose some columns
vt.setPrimaryKeyColumns(Collections.EMPTY_LIST);
vt.setEscapeSql(escapeSql);
ds.addVirtualTable(vt);
return guessFeatureType(ds, vt.getName(), guessGeometrySrid);
} finally {
if(vtName != null) {
ds.removeVirtualTable(name);
}
}
}
/**
* Grabs the feature type from the store, but takes a peek at figuring out the geoemtry type and
* srids
*
* @param schema
* @return
*/
SimpleFeatureType guessFeatureType(JDBCDataStore store, String vtName, boolean guessGeometrySrid) throws IOException {
SimpleFeatureType base = store.getSchema(vtName);
List<String> geometries = new ArrayList<String>();
for (AttributeDescriptor ad : base.getAttributeDescriptors()) {
if (ad instanceof GeometryDescriptor) {
geometries.add(ad.getLocalName());
}
}
// no geometries? Or, shall we not try to guess the geometries type and srid?
if (geometries.size() == 0 || !guessGeometrySrid) {
return base;
}
// build a query to fetch the first rwo, we'll inspect the resulting
// geometries
Query q = new Query(vtName);
q.setPropertyNames(geometries);
q.setMaxFeatures(1);
SimpleFeatureIterator it = null;
SimpleFeature f = null;
try {
it = store.getFeatureSource(vtName).getFeatures(q).features();
if (it.hasNext()) {
f = it.next();
}
} finally {
if (it != null) {
it.close();
}
}
// did we get more information?
if (f == null) {
return base;
}
// if so, try to build an override feature type
Connection cx = null;
try {
store.getConnection(Transaction.AUTO_COMMIT);
SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
tb.setName(base.getName());
for (AttributeDescriptor ad : base.getAttributeDescriptors()) {
if (ad instanceof GeometryDescriptor) {
GeometryDescriptor gd = (GeometryDescriptor) ad;
Geometry g = (Geometry) f.getAttribute(ad.getLocalName());
if (g == null) {
// nothing new we can learn
tb.add(ad);
} else {
Class binding = g.getClass();
CoordinateReferenceSystem crs = null;
if (g.getSRID() > 0) {
// see if the dialect can handle this one
crs = store.getSQLDialect().createCRS(g.getSRID(), cx);
tb.userData(JDBCDataStore.JDBC_NATIVE_SRID, g.getSRID());
}
if (crs == null) {
crs = gd.getCoordinateReferenceSystem();
}
tb.add(ad.getLocalName(), binding, crs);
}
} else {
tb.add(ad);
}
}
return tb.buildFeatureType();
} catch (SQLException e) {
throw (IOException) new IOException(e.getMessage()).initCause(e);
} finally {
store.closeSafe(cx);
}
}
protected VirtualTable buildVirtualTable() {
VirtualTable vt = new VirtualTable(name, sql);
attProvider.fillVirtualTable(vt);
paramProvider.updateVirtualTable(vt);
return vt;
}
/**
* Data stores tend to return IOExceptions with no explanation, and the actual error coming from
* the db is in the cause. This method extracts the first not null message in the cause chain
*
* @param t
* @return
*/
protected String getFirstErrorMessage(Throwable t) {
Throwable original = t;
while (!(t instanceof SQLException)) {
t = t.getCause();
if (t == null) {
break;
}
}
if(t == null) {
return original.getMessage();
} else {
return t.getMessage();
}
}
protected abstract void onSave();
protected abstract void onCancel();
/**
* Displays the geometry type in the geom type drop down
* @author Andrea Aime - OpenGeo
*/
static class GeometryTypeRenderer implements IChoiceRenderer {
public Object getDisplayValue(Object object) {
return ((Class) object).getSimpleName();
}
public String getIdValue(Object object, int index) {
return (String) getDisplayValue(object);
}
}
/**
* Validaes the regular expression syntax
*/
static class RegexpValidator extends AbstractValidator {
@Override
protected void onValidate(IValidatable validatable) {
String value = (String) validatable.getValue();
if(value != null) {
try {
Pattern.compile(value);
} catch(PatternSyntaxException e) {
Map<String, String> map = new HashMap<String, String>();
map.put("regexp", value);
map.put("error", e.getMessage().replaceAll("\\^?", ""));
error(validatable, "invalidRegexp", map);
}
}
}
}
/**
* Checks the sql view name is unique
*/
class ViewNameValidator extends AbstractValidator {
@Override
protected void onValidate(IValidatable validatable) {
String vtName = (String) validatable.getValue();
final DataStoreInfo store = getCatalog().getStore(storeId, DataStoreInfo.class);
List<FeatureTypeInfo> ftis = getCatalog().getResourcesByStore(store, FeatureTypeInfo.class);
for (FeatureTypeInfo curr : ftis) {
VirtualTable currvt = curr.getMetadata().get(FeatureTypeInfo.JDBC_VIRTUAL_TABLE, VirtualTable.class);
if(currvt != null) {
if(typeInfoId == null || !typeInfoId.equals(curr.getId())) {
if(currvt.getName().equals(vtName)) {
Map<String, String> map = new HashMap<String, String>();
map.put("name", vtName);
map.put("typeName", curr.getName());
error(validatable, "duplicateSqlViewName", map);
return;
}
}
}
}
}
}
@Override
protected ComponentAuthorizer getPageAuthorizer() {
return ComponentAuthorizer.WORKSPACE_ADMIN;
}
}