/*
* Constellation - An open source and standard compliant SDI
* http://www.constellation-sdi.org
*
* Copyright 2014 Geomatys.
*
* 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.constellation.sos.ws;
import com.vividsolutions.jts.geom.Geometry;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.util.logging.Logging;
import org.constellation.dto.SensorMLTree;
import org.constellation.util.ReflectionUtilities;
import org.geotoolkit.geometry.jts.JTS;
import org.geotoolkit.gml.xml.AbstractFeature;
import org.geotoolkit.gml.xml.AbstractGeometry;
import org.geotoolkit.gml.xml.BoundingShape;
import org.geotoolkit.gml.xml.Envelope;
import org.geotoolkit.observation.ObservationReader;
import org.geotoolkit.observation.ObservationStoreException;
import org.geotoolkit.referencing.CRS;
import org.geotoolkit.sml.xml.AbstractClassification;
import org.geotoolkit.sml.xml.AbstractClassifier;
import org.geotoolkit.sml.xml.AbstractComponents;
import org.geotoolkit.sml.xml.AbstractDerivableComponent;
import org.geotoolkit.sml.xml.AbstractIdentification;
import org.geotoolkit.sml.xml.AbstractIdentifier;
import org.geotoolkit.sml.xml.AbstractProcess;
import org.geotoolkit.sml.xml.AbstractProcessChain;
import org.geotoolkit.sml.xml.AbstractSensorML;
import org.geotoolkit.sml.xml.ComponentProperty;
import org.geotoolkit.sml.xml.SMLMember;
import org.geotoolkit.sml.xml.SensorMLMarshallerPool;
import org.geotoolkit.sml.xml.System;
import org.geotoolkit.sos.xml.SOSMarshallerPool;
import org.geotoolkit.sos.xml.SOSXmlFactory;
import org.geotoolkit.swe.xml.AbstractEncoding;
import org.geotoolkit.swe.xml.TextBlock;
import org.geotoolkit.temporal.object.ISODateParser;
import org.opengis.geometry.primitive.Point;
import org.opengis.observation.Observation;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.opengis.temporal.Period;
import org.opengis.util.FactoryException;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.lang.reflect.Method;
import java.net.URI;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.constellation.admin.SpringHelper;
import org.geotoolkit.gml.xml.FeatureProperty;
import org.geotoolkit.observation.xml.AbstractObservation;
import static org.geotoolkit.ows.xml.OWSExceptionCode.INVALID_PARAMETER_VALUE;
import static org.geotoolkit.ows.xml.OWSExceptionCode.MISSING_PARAMETER_VALUE;
import static org.geotoolkit.sml.xml.SensorMLUtilities.getSensorMLType;
import static org.geotoolkit.sml.xml.SensorMLUtilities.getSmlID;
/**
*
* @author Guilhem Legal (Geomatys)
*/
public final class SOSUtils {
/**
* use for debugging purpose
*/
private static final Logger LOGGER = Logging.getLogger("org.constellation.sos");
private static CoordinateReferenceSystem WGS84;
static {
try {
WGS84 = CRS.decode("CRS:84");
} catch (FactoryException ex) {
LOGGER.log(Level.WARNING, "Unable to retrieve CRS:84", ex);
}
}
private SOSUtils() {}
/**
* Return the physical ID of a sensor.
* This ID is found into a "Identifier" mark with the name 'supervisorCode'
*
* @param sensor
* @return
*/
public static String getPhysicalID(final AbstractSensorML sensor) {
if (sensor != null && sensor.getMember().size() > 0) {
final AbstractProcess process = sensor.getMember().get(0).getRealProcess();
final List<? extends AbstractIdentification> idents = process.getIdentification();
for(AbstractIdentification ident : idents) {
if (ident.getIdentifierList() != null) {
for (AbstractIdentifier identifier: ident.getIdentifierList().getIdentifier()) {
if ("supervisorCode".equals(identifier.getName()) && identifier.getTerm() != null) {
return identifier.getTerm().getValue();
}
}
}
}
}
return null;
}
/**
* Return the networks names binded to this sensor.
*
* * Those names are found into "Classifier" marks with the name 'network'
* @param sensor
* @return
*/
@Deprecated
public static List<String> getNetworkNames(final AbstractSensorML sensor) {
final List<String> results = new ArrayList<>();
if (sensor != null && sensor.getMember().size() == 1) {
final AbstractProcess component = sensor.getMember().get(0).getRealProcess();
if (component != null) {
for (AbstractClassification cl : component.getClassification()) {
if (cl.getClassifierList() != null) {
for (AbstractClassifier classifier : cl.getClassifierList().getClassifier()) {
if (classifier.getName().equals("network") && classifier.getTerm() != null) {
results.add(classifier.getTerm().getValue());
}
}
}
}
}
}
return results;
}
/**
* Return the position of a sensor.
*
* @param sensor
* @return
*/
public static AbstractGeometry getSensorPosition(final AbstractSensorML sensor) {
if (sensor.getMember().size() == 1) {
if (sensor.getMember().get(0).getRealProcess() instanceof AbstractDerivableComponent) {
final AbstractDerivableComponent component = (AbstractDerivableComponent) sensor.getMember().get(0).getRealProcess();
if (component.getSMLLocation() != null && component.getSMLLocation().getGeometry()!= null) {
return component.getSMLLocation().getGeometry();
} else if (component.getPosition() != null && component.getPosition().getPosition() != null &&
component.getPosition().getPosition().getLocation() != null && component.getPosition().getPosition().getLocation().getVector() != null) {
final URI crs = component.getPosition().getPosition().getReferenceFrame();
return component.getPosition().getPosition().getLocation().getVector().getGeometry(crs);
}
}
}
LOGGER.severe("there is no piezo location");
return null;
}
/**
* return a SQL formatted timestamp
*
* @param time a GML time position object.
* @return
* @throws org.geotoolkit.observation.ObservationStoreException
*/
public static String getTimeValue(final Date time) throws ObservationStoreException {
if (time != null) {
try {
// final String value = time.toString();
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SS");
final String value = df.format(time);
//here t is not used but it allow to verify the syntax of the timestamp
final ISODateParser parser = new ISODateParser();
final Date d = parser.parseToDate(value);
final Timestamp t = new Timestamp(d.getTime());
return t.toString();
} catch(IllegalArgumentException e) {
throw new ObservationStoreException("Unable to parse the value: " + time.toString() + '\n' +
"Bad format of timestamp:\n" + e.getMessage(),
INVALID_PARAMETER_VALUE, "eventTime");
}
} else {
String locator;
if (time == null) {
locator = "Timeposition";
} else {
locator = "TimePosition value";
}
throw new ObservationStoreException("bad format of time, " + locator + " mustn't be null",
MISSING_PARAMETER_VALUE, "eventTime");
}
}
public static Timestamp getTimestampValue(final Date time) throws ObservationStoreException {
return Timestamp.valueOf(getTimeValue(time));
}
/**
* return a SQL formatted timestamp
*
* @param time a GML time position object.
* @throws org.apache.sis.storage.DataStoreException
*/
public static String getLuceneTimeValue(final Date time) throws DataStoreException {
if (time != null) {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SS");
String value = df.format(time);
// String value = time.toString();
// we delete the data after the second TODO remove
if (value.indexOf('.') != -1) {
value = value.substring(0, value.indexOf('.'));
}
try {
// verify the syntax of the timestamp
//here t is not used but it allow to verify the syntax of the timestamp
final ISODateParser parser = new ISODateParser();
final Date d = parser.parseToDate(value);
} catch(IllegalArgumentException e) {
throw new ObservationStoreException("Unable to parse the value: " + value + '\n' +
"Bad format of timestamp:\n" + e.getMessage(),
INVALID_PARAMETER_VALUE, "eventTime");
}
value = value.replace(" ", "");
value = value.replace("-", "");
value = value.replace(":", "");
value = value.replace("T", "");
return value;
} else {
String locator;
if (time == null) {
locator = "Timeposition";
} else {
locator = "TimePosition value";
}
throw new ObservationStoreException("bad format of time, " + locator + " mustn't be null",
MISSING_PARAMETER_VALUE, "eventTime");
}
}
/**
* Transform a Lucene Date syntax string into a yyyy-MM-dd hh:mm:ss Date format String.
*
* @param luceneTimeValue A String on Lucene date format
* @return A String on yyy-MM-dd hh:mm:ss Date format
*/
public static String unLuceneTimeValue(String luceneTimeValue) {
final String year = luceneTimeValue.substring(0, 4);
luceneTimeValue = luceneTimeValue.substring(4);
final String month = luceneTimeValue.substring(0, 2);
luceneTimeValue = luceneTimeValue.substring(2);
final String day = luceneTimeValue.substring(0, 2);
luceneTimeValue = luceneTimeValue.substring(2);
final String hour = luceneTimeValue.substring(0, 2);
luceneTimeValue = luceneTimeValue.substring(2);
final String min = luceneTimeValue.substring(0, 2);
luceneTimeValue = luceneTimeValue.substring(2);
final String sec = luceneTimeValue.substring(0, 2);
return year + '-' + month + '-' + day + ' ' + hour + ':' + min + ':' + sec;
}
/**
* Return an envelope containing all the Observation member of the collection.
*
* @param version
* @param observations
* @param srsName
* @return
*/
public static Envelope getCollectionBound(final String version, final List<Observation> observations, final String srsName) {
double minx = Double.MAX_VALUE;
double miny = Double.MAX_VALUE;
double maxx = -Double.MAX_VALUE;
double maxy = -Double.MAX_VALUE;
for (Observation observation: observations) {
final AbstractFeature feature = (AbstractFeature) observation.getFeatureOfInterest();
if (feature != null) {
if (feature.getBoundedBy() != null) {
final BoundingShape bound = feature.getBoundedBy();
if (bound.getEnvelope() != null) {
if (bound.getEnvelope().getLowerCorner() != null
&& bound.getEnvelope().getLowerCorner().getCoordinate() != null
&& bound.getEnvelope().getLowerCorner().getCoordinate().length == 2 ) {
final double[] lower = bound.getEnvelope().getLowerCorner().getCoordinate();
if (lower[0] < minx) {
minx = lower[0];
}
if (lower[1] < miny) {
miny = lower[1];
}
}
if (bound.getEnvelope().getUpperCorner() != null
&& bound.getEnvelope().getUpperCorner().getCoordinate() != null
&& bound.getEnvelope().getUpperCorner().getCoordinate().length == 2 ) {
final double[] upper = bound.getEnvelope().getUpperCorner().getCoordinate();
if (upper[0] > maxx) {
maxx = upper[0];
}
if (upper[1] > maxy) {
maxy = upper[1];
}
}
}
}
}
}
if (minx == Double.MAX_VALUE) {
minx = -180.0;
}
if (miny == Double.MAX_VALUE) {
miny = -90.0;
}
if (maxx == (-Double.MAX_VALUE)) {
maxx = 180.0;
}
if (maxy == (-Double.MAX_VALUE)) {
maxy = 90.0;
}
final Envelope env = SOSXmlFactory.buildEnvelope(version, null, minx, miny, maxx, maxy, srsName);
env.setSrsDimension(2);
env.setAxisLabels(Arrays.asList("Y X"));
return env;
}
/**
* Used for CSV encoding, while iterating on a resultSet.
*
* if the round on the current date is over, and some field data are not present,
* we have to add empty token before to start the next date round.
*
* example : we are iterating on some date with temperature an salinity
*
* date | phenomenon | value
* 2010-01-01 TEMP 1
* 2010-01-01 SAL 202
* 2010-01-02 TEMP 3
* 2010-01-02 SAL 201
* 2010-01-03 TEMP 4
* 2010-01-04 TEMP 2
* 2010-01-04 SAL 210
*
* CSV encoding will be : @@2010-01-01,1,202@@2010-01-02,3,201@@2010-01-03,4,@@2010-01-04,2,210
*
* @param value the datablock builder.
* @param currentIndex the current object index.
*/
public static void fillEndingDataHoles(final Appendable value, int currentIndex, final List<String> fieldList, final TextBlock encoding, final int nbBlockByHole) throws IOException {
while (currentIndex < fieldList.size()) {
if (value != null) {
for (int i = 0; i < nbBlockByHole; i++) {
value.append(encoding.getTokenSeparator());
}
}
currentIndex++;
}
}
/**
* Used for CSV encoding, while iterating on a resultSet.
*
* if some field data are not present in the middle of a date round,
* we have to add empty token until we got the next phenomenon data.
*
* @param value the datablock builder.
* @param currentIndex the current phenomenon index.
* @param searchedField the name of the current phenomenon.
*
* @return the updated phenomenon index.
*/
public static int fillDataHoles(final Appendable value, int currentIndex, final String searchedField, final List<String> fieldList, final TextBlock encoding, final int nbBlockByHole) throws IOException {
while (currentIndex < fieldList.size() && !fieldList.get(currentIndex).equals(searchedField)) {
if (value != null) {
for (int i = 0; i < nbBlockByHole; i++) {
value.append(encoding.getTokenSeparator());
}
}
currentIndex++;
}
return currentIndex;
}
public static String getIDFromObject(final Object obj) {
if (obj != null) {
final Method idGetter = ReflectionUtilities.getGetterFromName("id", obj.getClass());
if (idGetter != null) {
return (String) ReflectionUtilities.invokeMethod(obj, idGetter);
}
}
return null;
}
public static Period extractTimeBounds(final String version, final String brutValues, final AbstractEncoding abstractEncoding) {
final String[] result = new String[2];
if (abstractEncoding instanceof TextBlock) {
final TextBlock encoding = (TextBlock) abstractEncoding;
final StringTokenizer tokenizer = new StringTokenizer(brutValues, encoding.getBlockSeparator());
boolean first = true;
while (tokenizer.hasMoreTokens()) {
final String block = tokenizer.nextToken();
final int tokenEnd = block.indexOf(encoding.getTokenSeparator());
String samplingTimeValue;
if (tokenEnd != -1) {
samplingTimeValue = block.substring(0, block.indexOf(encoding.getTokenSeparator()));
// only one field
} else {
samplingTimeValue = block;
}
if (first) {
result[0] = samplingTimeValue;
first = false;
} else if (!tokenizer.hasMoreTokens()) {
result[1] = samplingTimeValue;
}
}
} else {
LOGGER.warning("unable to parse datablock unknown encoding");
}
return SOSXmlFactory.buildTimePeriod(version, null, result[0], result[1]);
}
/**
* Return true if the samplingPoint entry is strictly inside the specified envelope.
*
* @param sp A sampling point (2D) station.
* @param e An envelope (2D).
* @return True if the sampling point is strictly inside the specified envelope.
*/
public static boolean samplingPointMatchEnvelope(final Point sp, final Envelope e) {
if (sp.getDirectPosition() != null) {
final double stationX = sp.getDirectPosition().getOrdinate(0);
final double stationY = sp.getDirectPosition().getOrdinate(1);
final double minx = e.getLowerCorner().getOrdinate(0);
final double maxx = e.getUpperCorner().getOrdinate(0);
final double miny = e.getLowerCorner().getOrdinate(1);
final double maxy = e.getUpperCorner().getOrdinate(1);
// we look if the station if contained in the BBOX
return stationX < maxx && stationX > minx && stationY < maxy && stationY > miny;
}
LOGGER.log(Level.WARNING, " the feature of interest does not have proper position");
return false;
}
public static boolean BoundMatchEnvelope(final AbstractFeature sc, final Envelope e) {
if (sc.getBoundedBy() != null &&
sc.getBoundedBy().getEnvelope() != null &&
sc.getBoundedBy().getEnvelope().getLowerCorner() != null &&
sc.getBoundedBy().getEnvelope().getUpperCorner() != null &&
sc.getBoundedBy().getEnvelope().getLowerCorner().getCoordinate().length > 1 &&
sc.getBoundedBy().getEnvelope().getUpperCorner().getCoordinate().length > 1) {
final double stationMinX = sc.getBoundedBy().getEnvelope().getLowerCorner().getOrdinate(0);
final double stationMaxX = sc.getBoundedBy().getEnvelope().getUpperCorner().getOrdinate(0);
final double stationMinY = sc.getBoundedBy().getEnvelope().getLowerCorner().getOrdinate(1);
final double stationMaxY = sc.getBoundedBy().getEnvelope().getUpperCorner().getOrdinate(1);
final double minx = e.getLowerCorner().getOrdinate(0);
final double maxx = e.getUpperCorner().getOrdinate(0);
final double miny = e.getLowerCorner().getOrdinate(1);
final double maxy = e.getUpperCorner().getOrdinate(1);
// we look if the station if contained in the BBOX
if (stationMaxX < maxx && stationMinX > minx &&
stationMaxY < maxy && stationMinY > miny) {
return true;
} else {
LOGGER.log(Level.FINER, " the feature of interest {0} is not in the BBOX", sc.getId());
}
} else {
LOGGER.log(Level.WARNING, " the feature of interest (samplingCurve){0} does not have proper bounds", sc.getId());
}
return false;
}
public static void removeComponent(final AbstractSensorML sml, final String component) {
if (sml.getMember() != null) {
//assume only one member
for (SMLMember member : sml.getMember()) {
final AbstractProcess process = member.getRealProcess();
if (process instanceof System) {
final System s = (System) process;
final AbstractComponents compos = s.getComponents();
if (compos != null && compos.getComponentList() != null) {
compos.getComponentList().removeComponent(component);
}
}
}
}
}
public static List<SensorMLTree> getChildren(final AbstractSensorML sml) {
if (sml.getMember() != null) {
//assume only one member
for (SMLMember member : sml.getMember()) {
final AbstractProcess process = member.getRealProcess();
return getChildren(process);
}
}
return new ArrayList<>();
}
public static List<SensorMLTree> getChildren(final AbstractProcess process) {
final List<SensorMLTree> results = new ArrayList<>();
if (process instanceof System) {
final System s = (System) process;
final AbstractComponents compos = s.getComponents();
if (compos != null && compos.getComponentList() != null) {
for (ComponentProperty cp : compos.getComponentList().getComponent()){
if (cp.getHref() != null) {
results.add(new SensorMLTree(cp.getHref(), "unknown", null, null));
} else if (cp.getAbstractProcess()!= null) {
results.add(new SensorMLTree(getSmlID(cp.getAbstractProcess()), getSensorMLType(cp.getAbstractProcess()), null, null));
} else {
LOGGER.warning("SML system component has no href or embedded object");
}
}
}
} else if (process instanceof AbstractProcessChain) {
final AbstractProcessChain s = (AbstractProcessChain) process;
final AbstractComponents compos = s.getComponents();
if (compos != null && compos.getComponentList() != null) {
for (ComponentProperty cp : compos.getComponentList().getComponent()){
if (cp.getHref() != null) {
results.add(new SensorMLTree(cp.getHref(), "unknown", null, null));
} else if (cp.getAbstractProcess()!= null) {
results.add(new SensorMLTree(getSmlID(cp.getAbstractProcess()), getSensorMLType(cp.getAbstractProcess()),null, null));
} else {
LOGGER.warning("SML system component has no href or embedded object");
}
}
}
}
return results;
}
public static List<Geometry> getJTSGeometryFromSensor(final SensorMLTree sensor, final ObservationReader reader) throws DataStoreException, FactoryException, TransformException {
if ("Component".equals(sensor.getType())) {
final AbstractGeometry geom = reader.getSensorLocation(sensor.getId(), "2.0.0");
if (geom != null) {
Geometry jtsGeometry = GeometrytoJTS.toJTS(geom);
// reproject to CRS:84
final MathTransform mt = CRS.findMathTransform(geom.getCoordinateReferenceSystem(true), WGS84);
return Arrays.asList(JTS.transform(jtsGeometry, mt));
}
} else {
final List<Geometry> geometries = new ArrayList<>();
// add the root geometry if there is one
final AbstractGeometry geom = reader.getSensorLocation(sensor.getId(), "2.0.0");
if (geom != null) {
Geometry jtsGeometry = GeometrytoJTS.toJTS(geom);
// reproject to CRS:84
final MathTransform mt = CRS.findMathTransform(geom.getCoordinateReferenceSystem(true), WGS84);
geometries.add(JTS.transform(jtsGeometry, mt));
}
for (SensorMLTree child : sensor.getChildren()) {
geometries.addAll(getJTSGeometryFromSensor(child, reader));
}
return geometries;
}
return new ArrayList<>();
}
public static Collection<String> getPhenomenonFromSensor(final SensorMLTree sensor, final ObservationReader reader) throws DataStoreException {
if ("Component".equals(sensor.getType())) {
return reader.getPhenomenonsForProcedure(sensor.getId());
} else {
final Set<String> phenomenons = new HashSet<>();
// add the root phenomenon if there is one
phenomenons.addAll(reader.getPhenomenonsForProcedure(sensor.getId()));
for (SensorMLTree child : sensor.getChildren()) {
phenomenons.addAll(getPhenomenonFromSensor(child, reader));
}
return phenomenons;
}
}
public static AbstractSensorML unmarshallSensor(final File f) throws JAXBException, DataStoreException {
final Unmarshaller um = SensorMLMarshallerPool.getInstance().acquireUnmarshaller();
Object obj = um.unmarshal(f);
if (obj instanceof JAXBElement) {
obj = ((JAXBElement)obj).getValue();
}
if (obj instanceof AbstractSensorML) {
return (AbstractSensorML)obj;
}
throw new DataStoreException("the sensorML file does not contain a valid sensorML object");
}
public static AbstractSensorML unmarshallSensor(final InputStream is) throws JAXBException, DataStoreException {
final Unmarshaller um = SensorMLMarshallerPool.getInstance().acquireUnmarshaller();
Object obj = um.unmarshal(is);
if (obj instanceof JAXBElement) {
obj = ((JAXBElement)obj).getValue();
}
if (obj instanceof AbstractSensorML) {
return (AbstractSensorML)obj;
}
throw new DataStoreException("the sensorML file does not contain a valid sensorML object");
}
public static AbstractSensorML unmarshallSensor(final String xml) throws JAXBException, DataStoreException {
final Unmarshaller um = SensorMLMarshallerPool.getInstance().acquireUnmarshaller();
Object obj = um.unmarshal(new StringReader(xml));
if (obj instanceof JAXBElement) {
obj = ((JAXBElement)obj).getValue();
}
if (obj instanceof AbstractSensorML) {
return (AbstractSensorML)obj;
}
throw new DataStoreException("the sensorML file does not contain a valid sensorML object");
}
public static Object unmarshallObservationFile(final File f) throws JAXBException, DataStoreException {
final Unmarshaller um = SOSMarshallerPool.getInstance().acquireUnmarshaller();
Object obj = um.unmarshal(f);
if (obj instanceof JAXBElement) {
obj = ((JAXBElement)obj).getValue();
}
if (obj != null) {
return obj;
}
throw new DataStoreException("the observation file does not contain a valid O&M object");
}
public static boolean isCompleteEnvelope3D(Envelope e) {
return e.getLowerCorner() != null && e.getUpperCorner() != null
&& e.getLowerCorner().getCoordinate().length == 3 && e.getUpperCorner().getCoordinate().length == 3;
}
public static String extractFOID(Observation obs) {
if (obs.getFeatureOfInterest() instanceof AbstractFeature) {
return ((AbstractFeature)obs.getFeatureOfInterest()).getId();
} else if (obs instanceof AbstractObservation) {
AbstractObservation aobs = (AbstractObservation) obs;
FeatureProperty featProp = aobs.getPropertyFeatureOfInterest();
return featProp.getHref();
}
return null;
}
}