001package ca.uhn.fhir.jpa.dao.predicate; 002 003/*- 004 * #%L 005 * HAPI FHIR JPA Server 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.context.RuntimeSearchParam; 025import ca.uhn.fhir.interceptor.model.RequestPartitionId; 026import ca.uhn.fhir.jpa.dao.LegacySearchBuilder; 027import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords; 028import ca.uhn.fhir.jpa.util.CoordCalculator; 029import ca.uhn.fhir.model.api.IQueryParameterType; 030import ca.uhn.fhir.model.dstu2.resource.Location; 031import ca.uhn.fhir.rest.param.QuantityParam; 032import ca.uhn.fhir.rest.param.SpecialParam; 033import ca.uhn.fhir.rest.param.TokenParam; 034import com.google.common.annotations.VisibleForTesting; 035import org.hibernate.search.engine.spatial.GeoBoundingBox; 036import org.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038import org.springframework.context.annotation.Scope; 039import org.springframework.stereotype.Component; 040 041import javax.persistence.criteria.CriteriaBuilder; 042import javax.persistence.criteria.From; 043import javax.persistence.criteria.Predicate; 044import java.util.ArrayList; 045import java.util.List; 046 047import static org.apache.commons.lang3.StringUtils.isBlank; 048 049@Component 050@Scope("prototype") 051public class PredicateBuilderCoords extends BasePredicateBuilder implements IPredicateBuilder { 052 private static final Logger ourLog = LoggerFactory.getLogger(PredicateBuilderCoords.class); 053 054 public PredicateBuilderCoords(LegacySearchBuilder theSearchBuilder) { 055 super(theSearchBuilder); 056 } 057 058 private Predicate createPredicateCoords(IQueryParameterType theParam, 059 String theResourceName, 060 RuntimeSearchParam theSearchParam, 061 CriteriaBuilder theBuilder, 062 From<?, ResourceIndexedSearchParamCoords> theFrom, 063 RequestPartitionId theRequestPartitionId) { 064 String latitudeValue; 065 String longitudeValue; 066 Double distanceKm = 0.0; 067 068 if (theParam instanceof TokenParam) { // DSTU3 069 TokenParam param = (TokenParam) theParam; 070 String value = param.getValue(); 071 String[] parts = value.split(":"); 072 if (parts.length != 2) { 073 throw new IllegalArgumentException(Msg.code(1038) + "Invalid position format '" + value + "'. Required format is 'latitude:longitude'"); 074 } 075 latitudeValue = parts[0]; 076 longitudeValue = parts[1]; 077 if (isBlank(latitudeValue) || isBlank(longitudeValue)) { 078 throw new IllegalArgumentException(Msg.code(1039) + "Invalid position format '" + value + "'. Both latitude and longitude must be provided."); 079 } 080 QuantityParam distanceParam = myParams.getNearDistanceParam(); 081 if (distanceParam != null) { 082 distanceKm = distanceParam.getValue().doubleValue(); 083 } 084 } else if (theParam instanceof SpecialParam) { // R4 085 SpecialParam param = (SpecialParam) theParam; 086 String value = param.getValue(); 087 String[] parts = value.split("\\|"); 088 if (parts.length < 2 || parts.length > 4) { 089 throw new IllegalArgumentException(Msg.code(1040) + "Invalid position format '" + value + "'. Required format is 'latitude|longitude' or 'latitude|longitude|distance' or 'latitude|longitude|distance|units'"); 090 } 091 latitudeValue = parts[0]; 092 longitudeValue = parts[1]; 093 if (isBlank(latitudeValue) || isBlank(longitudeValue)) { 094 throw new IllegalArgumentException(Msg.code(1041) + "Invalid position format '" + value + "'. Both latitude and longitude must be provided."); 095 } 096 if (parts.length >= 3) { 097 String distanceString = parts[2]; 098 if (!isBlank(distanceString)) { 099 distanceKm = Double.valueOf(distanceString); 100 } 101 } 102 } else { 103 throw new IllegalArgumentException(Msg.code(1042) + "Invalid position type: " + theParam.getClass()); 104 } 105 106 Predicate latitudePredicate; 107 Predicate longitudePredicate; 108 if (distanceKm == 0.0) { 109 latitudePredicate = theBuilder.equal(theFrom.get("myLatitude"), latitudeValue); 110 longitudePredicate = theBuilder.equal(theFrom.get("myLongitude"), longitudeValue); 111 } else if (distanceKm < 0.0) { 112 throw new IllegalArgumentException(Msg.code(1043) + "Invalid " + Location.SP_NEAR_DISTANCE + " parameter '" + distanceKm + "' must be >= 0.0"); 113 } else if (distanceKm > CoordCalculator.MAX_SUPPORTED_DISTANCE_KM) { 114 throw new IllegalArgumentException(Msg.code(1044) + "Invalid " + Location.SP_NEAR_DISTANCE + " parameter '" + distanceKm + "' must be <= " + CoordCalculator.MAX_SUPPORTED_DISTANCE_KM); 115 } else { 116 double latitudeDegrees = Double.parseDouble(latitudeValue); 117 double longitudeDegrees = Double.parseDouble(longitudeValue); 118 119 GeoBoundingBox box = CoordCalculator.getBox(latitudeDegrees, longitudeDegrees, distanceKm); 120 latitudePredicate = latitudePredicateFromBox(theBuilder, theFrom, box); 121 longitudePredicate = longitudePredicateFromBox(theBuilder, theFrom, box); 122 } 123 Predicate singleCode = theBuilder.and(latitudePredicate, longitudePredicate); 124 return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theSearchParam.getName(), theFrom, singleCode, theRequestPartitionId); 125 } 126 127 private Predicate latitudePredicateFromBox(CriteriaBuilder theBuilder, From<?, ResourceIndexedSearchParamCoords> theFrom, GeoBoundingBox theBox) { 128 return theBuilder.and( 129 theBuilder.greaterThanOrEqualTo(theFrom.get("myLatitude"), theBox.bottomRight().latitude()), 130 theBuilder.lessThanOrEqualTo(theFrom.get("myLatitude"), theBox.topLeft().latitude()) 131 ); 132 } 133 134 @VisibleForTesting 135 Predicate longitudePredicateFromBox(CriteriaBuilder theBuilder, From<?, ResourceIndexedSearchParamCoords> theFrom, GeoBoundingBox theBox) { 136 if (theBox.bottomRight().longitude() < theBox.topLeft().longitude()) { 137 return theBuilder.or( 138 theBuilder.greaterThanOrEqualTo(theFrom.get("myLongitude"), theBox.bottomRight().longitude()), 139 theBuilder.lessThanOrEqualTo(theFrom.get("myLongitude"), theBox.topLeft().longitude()) 140 ); 141 } 142 return theBuilder.and( 143 theBuilder.greaterThanOrEqualTo(theFrom.get("myLongitude"), theBox.topLeft().longitude()), 144 theBuilder.lessThanOrEqualTo(theFrom.get("myLongitude"), theBox.bottomRight().longitude()) 145 ); 146 } 147 148 @Override 149 public Predicate addPredicate(String theResourceName, 150 RuntimeSearchParam theSearchParam, 151 List<? extends IQueryParameterType> theList, 152 SearchFilterParser.CompareOperation theOperation, 153 RequestPartitionId theRequestPartitionId) { 154 From<?, ResourceIndexedSearchParamCoords> join = myQueryStack.createJoin(SearchBuilderJoinEnum.COORDS, theSearchParam.getName()); 155 156 if (theList.get(0).getMissing() != null) { 157 addPredicateParamMissingForNonReference(theResourceName, theSearchParam.getName(), theList.get(0).getMissing(), join, theRequestPartitionId); 158 return null; 159 } 160 161 List<Predicate> codePredicates = new ArrayList<>(); 162 addPartitionIdPredicate(theRequestPartitionId, join, codePredicates); 163 164 for (IQueryParameterType nextOr : theList) { 165 166 Predicate singleCode = createPredicateCoords(nextOr, 167 theResourceName, 168 theSearchParam, 169 myCriteriaBuilder, 170 join, 171 theRequestPartitionId); 172 codePredicates.add(singleCode); 173 } 174 175 Predicate retVal = myCriteriaBuilder.or(toArray(codePredicates)); 176 myQueryStack.addPredicateWithImplicitTypeSelection(retVal); 177 return retVal; 178 } 179}