Skip to content

Commit

Permalink
[GEOS-11306] Java 17 does not support GetFeature lazy JDBC count(*)
Browse files Browse the repository at this point in the history
  • Loading branch information
jodygarnett committed Feb 28, 2024
1 parent a14039e commit 502fc21
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 52 deletions.
66 changes: 31 additions & 35 deletions src/wfs/src/main/java/org/geoserver/wfs/GetFeature.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
Expand All @@ -26,7 +27,6 @@
import net.opengis.wfs20.QueryType;
import net.opengis.wfs20.ResultTypeType;
import net.opengis.wfs20.StoredQueryType;
import org.apache.commons.lang3.SystemUtils;
import org.geoserver.catalog.AttributeTypeInfo;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.FeatureTypeInfo;
Expand Down Expand Up @@ -120,8 +120,6 @@
import org.geotools.util.factory.Hints;
import org.geotools.xsd.Encoder;
import org.locationtech.jts.geom.Polygon;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.LazyLoader;
import org.xml.sax.helpers.NamespaceSupport;

/**
Expand Down Expand Up @@ -305,7 +303,7 @@ public FeatureCollectionResponse run(GetFeatureRequest request) throws WFSExcept

boolean isNumberMatchedSkipped = false;
int count = 0; // should probably be long
BigInteger totalCount = BigInteger.ZERO;
Supplier<BigInteger> totalCount = () -> BigInteger.ZERO;

// offset into result set in which to return features
int totalOffset = request.getStartIndex() != null ? request.getStartIndex().intValue() : -1;
Expand Down Expand Up @@ -682,50 +680,48 @@ private void validateQueryAliases(GetFeatureRequest request, Query query) {
}
}

private BigInteger updateTotalCount(
/**
* Total count represents the total count of the features matched for this query in cases where
* the client has limited the result set size, so we compute it lazily.
*
* @param maxFeatures
* @param isNumberMatchedSkipped
* @param count
* @param totalOffset
* @param calculateSize
* @param totalCountExecutors
* @return Lazy calculation of total count, or {@code null} if isNumberMatchedSkipped
* @throws IOException
*/
private Supplier<BigInteger> updateTotalCount(
int maxFeatures,
boolean isNumberMatchedSkipped,
int count,
int totalOffset,
boolean calculateSize,
List<CountExecutor> totalCountExecutors)
throws IOException {
BigInteger totalCount;
// total count represents the total count of the features matched for this query in
// cases/ where the client has limited the result set size, so we compute it lazily
if (isNumberMatchedSkipped) {
totalCount = BigInteger.valueOf(-1);
return () -> BigInteger.valueOf(-1);
} else if (count < maxFeatures && calculateSize && totalOffset == 0) {
// optimization: if count < max features then total count == count
// can't use this optimization for v2
totalCount = BigInteger.valueOf(count);
return () -> BigInteger.valueOf(count);
} else if (isPreComputed(totalCountExecutors)) {
long total = getTotalCount(totalCountExecutors);
totalCount = BigInteger.valueOf(total);
return () -> BigInteger.valueOf(total);
} else {
if (SystemUtils.IS_JAVA_11 || SystemUtils.IS_JAVA_1_8) {
// ok, in this case we're forced to run the queries to discover the actual total
// count. We do so lazily, not all output formats need it, leveraging the fact that
// BigInteger is not final to wrap it in a lazy loading proxy
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(BigInteger.class);
enhancer.setCallback(
(LazyLoader)
() -> {
long totalCount1 = getTotalCount(totalCountExecutors);
return BigInteger.valueOf(totalCount1);
});
totalCount =
(BigInteger)
enhancer.create(new Class[] {String.class}, new Object[] {"0"});
} else {
// Spring asm does not work with newer versions of Java, skip lazy loading for now
// we might want to revisit with a different approach later, e..g, when upgrading
// Spring and its internal ASM library
totalCount = BigInteger.valueOf(getTotalCount(totalCountExecutors));
}
return () -> {
try {
long totalCount1 = getTotalCount(totalCountExecutors);
return BigInteger.valueOf(totalCount1);
} catch (IOException ioException) {
throw new RuntimeException(
"Lazy total count unavailable " + ioException.getMessage(),
ioException);
}
};
}
return totalCount;
}

private void collectPropertyNames(
Expand Down Expand Up @@ -1020,14 +1016,14 @@ protected FeatureCollectionResponse buildResults(
int offset,
int maxFeatures,
int count,
BigInteger total,
Supplier<BigInteger> total,
List<FeatureCollection<? extends FeatureType, ? extends Feature>> results,
String lockId,
boolean getFeatureById) {

FeatureCollectionResponse result = request.createResponse();
result.setNumberOfFeatures(BigInteger.valueOf(count));
result.setTotalNumberOfFeatures(total);
result.setLazyTotalNumberOfFeatures(total);
result.setTimeStamp(Calendar.getInstance());
result.setLockId(lockId);
result.getFeature().addAll(results);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.math.BigInteger;
import java.util.Calendar;
import java.util.List;
import java.util.function.Supplier;
import net.opengis.wfs.FeatureCollectionType;
import net.opengis.wfs.WfsFactory;
import net.opengis.wfs20.Wfs20Factory;
Expand All @@ -22,7 +23,14 @@
*/
public abstract class FeatureCollectionResponse extends RequestObject {

private boolean getFeatureById = false;
protected boolean getFeatureById = false;

/**
* It can be expensive to determine the total number of features up front, by using a supplier
* we can defer calculation until the end of the request (when the value may already have been
* established).
*/
protected Supplier<BigInteger> lazyTotalNumberOfFeatures = null;

public static FeatureCollectionResponse adapt(Object adaptee) {
if (adaptee instanceof FeatureCollectionType) {
Expand Down Expand Up @@ -53,15 +61,66 @@ public void setTimeStamp(Calendar timeStamp) {
eSet(adaptee, "timeStamp", timeStamp);
}

/**
* Factory method creating a new FeatureCollectionResponse.
*
* @return feature collection response
*/
public abstract FeatureCollectionResponse create();

/**
* Number of features included in this response. Number reflect the number of features included,
* which may be less than {@link #getTotalNumberOfFeatures()} when paging through more content
* than can be obtained in a single request.
*
* @return number of features included in this response.
*/
public abstract BigInteger getNumberOfFeatures();

/**
* Number of features included in response.
*
* @param n Number of features included in response
*/
public abstract void setNumberOfFeatures(BigInteger n);

public abstract BigInteger getTotalNumberOfFeatures();
/**
* Used to calculate total number of features on demand (only if needed). This allows formats
* that do not need the total to avoid calculating this expensive result, it also may be that
* some data stores can better estimate this total is obtained after traversing results.
*
* @param totalNumberOfFeatures Delayed calculation of total number of featuers.
*/
public void setLazyTotalNumberOfFeatures(Supplier<BigInteger> totalNumberOfFeatures) {
this.lazyTotalNumberOfFeatures = totalNumberOfFeatures;
}

public abstract void setTotalNumberOfFeatures(BigInteger n);
/**
* Total number of features hits matched, or {@code null} for "unknown". Total is used as a
* guide when paging through more content than can be obtained in a single request.
*
* <p>This value is set by calling {@link #setLazyTotalNumberOfFeatures(Supplier)} (deferred
* value), or {@link #setTotalNumberOfFeatures(BigInteger)}.
*
* @return total number of features available, or null for "unknown".
*/
public BigInteger getTotalNumberOfFeatures() {
if (lazyTotalNumberOfFeatures != null) {
return lazyTotalNumberOfFeatures.get();
} else {
return null;
}
}

/**
* Total number of Features hits matched, which may be greater than the number included in an
* individual result.
*
* @param totalHits total number of feature hits matched, or {@code null} for "unknown".
*/
public void setTotalNumberOfFeatures(final BigInteger totalHits) {
lazyTotalNumberOfFeatures = () -> totalHits;
}

public abstract void setPrevious(String previous);

Expand Down Expand Up @@ -90,8 +149,8 @@ public boolean isGetFeatureById() {
return getFeatureById;
}

/** FeatureCollection response adapted from {@link net.opengis.wfs20.FeatureCollectionType}. */
public static class WFS11 extends FeatureCollectionResponse {
BigInteger totalNumberOfFeatures;

public WFS11(EObject adaptee) {
super(adaptee);
Expand All @@ -113,16 +172,6 @@ public void setNumberOfFeatures(BigInteger n) {
eSet(adaptee, "numberOfFeatures", n);
}

@Override
public BigInteger getTotalNumberOfFeatures() {
return totalNumberOfFeatures;
}

@Override
public void setTotalNumberOfFeatures(BigInteger n) {
this.totalNumberOfFeatures = n;
}

@Override
public String getPrevious() {
// noop
Expand Down Expand Up @@ -177,6 +226,7 @@ public Object unadapt(Class<?> target) {
}
}

/** FeatureCollectionResponse from {@link net.opengis.wfs20.FeatureCollectionType}. */
public static class WFS20 extends FeatureCollectionResponse {
public WFS20(EObject adaptee) {
super(adaptee);
Expand All @@ -200,14 +250,29 @@ public void setNumberOfFeatures(BigInteger n) {

@Override
public BigInteger getTotalNumberOfFeatures() {
BigInteger result = eGet(adaptee, "numberMatched", BigInteger.class);
if (result != null && result.signum() < 0) return null;
return result;
if (lazyTotalNumberOfFeatures != null) {
return lazyTotalNumberOfFeatures.get();
} else {
BigInteger result = eGet(adaptee, "numberMatched", BigInteger.class);
if (result != null && result.signum() < 0) {
// indicates "unknown"
return null;
}
return result;
}
}

@Override
public void setTotalNumberOfFeatures(BigInteger n) {
eSet(adaptee, "numberMatched", n);
this.lazyTotalNumberOfFeatures =
() -> {
BigInteger result = eGet(adaptee, "numberMatched", BigInteger.class);
if (result != null && result.signum() < 0) {
return null; // indicates "unknown"
}
return result;
};
}

@Override
Expand Down Expand Up @@ -245,6 +310,7 @@ public void setFeatures(List<FeatureCollection> features) {
@SuppressWarnings("unchecked") // EMF model without generics
public Object unadapt(Class<?> target) {
if (target.equals(net.opengis.wfs20.FeatureCollectionType.class)) {
eSet(adaptee, "numberMatched", getTotalNumberOfFeatures());
return adaptee;
} else if (target.equals(FeatureCollectionType.class)) {
net.opengis.wfs20.FeatureCollectionType source =
Expand Down

0 comments on commit 502fc21

Please sign in to comment.