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}