1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.healthconnect.storage.utils;
18 
19 import com.android.server.healthconnect.storage.request.ReadTableRequest;
20 
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.List;
24 import java.util.stream.Collectors;
25 
26 /** @hide */
27 public final class WhereClauses {
28     public enum LogicalOperator {
29         AND(" AND "),
30         OR(" OR ");
31 
32         private final String opKeyword;
33 
LogicalOperator(String opKeyword)34         LogicalOperator(String opKeyword) {
35             this.opKeyword = opKeyword;
36         }
37     }
38 
39     private final List<String> mClauses = new ArrayList<>();
40     private final LogicalOperator mLogicalOperator;
41 
WhereClauses(LogicalOperator logicalOperator)42     public WhereClauses(LogicalOperator logicalOperator) {
43         mLogicalOperator = logicalOperator;
44     }
45 
addWhereBetweenClause(String columnName, long start, long end)46     public WhereClauses addWhereBetweenClause(String columnName, long start, long end) {
47         mClauses.add(columnName + " BETWEEN " + start + " AND " + end);
48 
49         return this;
50     }
51 
addWhereBetweenTimeClause(String columnName, long startTime, long endTime)52     public WhereClauses addWhereBetweenTimeClause(String columnName, long startTime, long endTime) {
53         if (endTime < 0 || endTime < startTime) {
54             // Below method has check for startTime less than 0.
55             // If both startTime and endTime are less than 0 then no new time clause will be added
56             // and just return current object.
57             return addWhereLaterThanTimeClause(columnName, startTime);
58         }
59 
60         mClauses.add(columnName + " BETWEEN " + startTime + " AND " + endTime);
61 
62         return this;
63     }
64 
addWhereLaterThanTimeClause(String columnName, long startTime)65     public WhereClauses addWhereLaterThanTimeClause(String columnName, long startTime) {
66         if (startTime < 0) {
67             return this;
68         }
69 
70         mClauses.add(columnName + " > " + startTime);
71 
72         return this;
73     }
74 
addWhereInClause(String columnName, List<String> values)75     public WhereClauses addWhereInClause(String columnName, List<String> values) {
76         if (values == null || values.isEmpty()) return this;
77 
78         mClauses.add(columnName + " IN " + "('" + String.join("', '", values) + "')");
79 
80         return this;
81     }
82 
addWhereInClauseWithoutQuotes(String columnName, List<String> values)83     public WhereClauses addWhereInClauseWithoutQuotes(String columnName, List<String> values) {
84         if (values == null || values.isEmpty()) return this;
85 
86         mClauses.add(columnName + " IN " + "(" + String.join(", ", values) + ")");
87 
88         return this;
89     }
90 
addWhereEqualsClause(String columnName, String value)91     public WhereClauses addWhereEqualsClause(String columnName, String value) {
92         if (columnName == null || value == null || value.isEmpty() || columnName.isEmpty()) {
93             return this;
94         }
95 
96         mClauses.add(columnName + " = " + StorageUtils.getNormalisedString(value));
97         return this;
98     }
99 
addWhereGreaterThanClause(String columnName, String value)100     public WhereClauses addWhereGreaterThanClause(String columnName, String value) {
101         mClauses.add(columnName + " > '" + value + "'");
102 
103         return this;
104     }
105 
106     /** Add clause columnName > value */
addWhereGreaterThanClause(String columnName, long value)107     public WhereClauses addWhereGreaterThanClause(String columnName, long value) {
108         mClauses.add(columnName + " > " + value);
109 
110         return this;
111     }
112 
addWhereGreaterThanOrEqualClause(String columnName, long value)113     public WhereClauses addWhereGreaterThanOrEqualClause(String columnName, long value) {
114         mClauses.add(columnName + " >= " + value);
115 
116         return this;
117     }
118 
addWhereLessThanOrEqualClause(String columnName, long value)119     public WhereClauses addWhereLessThanOrEqualClause(String columnName, long value) {
120         mClauses.add(columnName + " <= " + value);
121 
122         return this;
123     }
124 
125     /** Add clause columnName < value */
addWhereLessThanClause(String columnName, long value)126     public WhereClauses addWhereLessThanClause(String columnName, long value) {
127         mClauses.add(columnName + " < " + value);
128 
129         return this;
130     }
131 
addWhereInIntsClause(String columnName, List<Integer> values)132     public WhereClauses addWhereInIntsClause(String columnName, List<Integer> values) {
133         if (values == null || values.isEmpty()) return this;
134 
135         mClauses.add(
136                 columnName
137                         + " IN ("
138                         + values.stream().map(String::valueOf).collect(Collectors.joining(", "))
139                         + ")");
140 
141         return this;
142     }
143 
144     /**
145      * Adds where in condition for the column.
146      *
147      * @param columnName Column name on which where condition to be applied
148      * @param values to check in the where condition
149      */
addWhereInLongsClause(String columnName, Collection<Long> values)150     public WhereClauses addWhereInLongsClause(String columnName, Collection<Long> values) {
151         if (values == null || values.isEmpty()) return this;
152 
153         mClauses.add(
154                 columnName
155                         + " IN ("
156                         + values.stream()
157                                 .distinct()
158                                 .map(String::valueOf)
159                                 .collect(Collectors.joining(", "))
160                         + ")");
161 
162         return this;
163     }
164 
165     /**
166      * Creates IN clause, where in range is another SQL request. Returns instance with extra clauses
167      * set.
168      */
addWhereInSQLRequestClause(String columnName, ReadTableRequest inRequest)169     public WhereClauses addWhereInSQLRequestClause(String columnName, ReadTableRequest inRequest) {
170         mClauses.add(columnName + " IN (" + inRequest.getReadCommand() + ") ");
171 
172         return this;
173     }
174 
175     /** Adds other {@link WhereClauses} as conditions of this where clause. */
addNestedWhereClauses(WhereClauses... otherWhereClauses)176     public WhereClauses addNestedWhereClauses(WhereClauses... otherWhereClauses) {
177         for (WhereClauses whereClauses : otherWhereClauses) {
178             if (whereClauses.mClauses.isEmpty()) {
179                 // skip empty WhereClauses so we don't add empty pairs of parentheses "()" to the
180                 // final SQL statement
181                 continue;
182             }
183             mClauses.add("(" + whereClauses.get(/* withWhereKeyword= */ false) + ")");
184         }
185 
186         return this;
187     }
188 
189     /**
190      * Returns where clauses joined by 'AND', if the input parameter isIncludeWHEREinClauses is true
191      * then the clauses are preceded by 'WHERE'.
192      */
get(boolean withWhereKeyword)193     public String get(boolean withWhereKeyword) {
194         if (mClauses.isEmpty()) {
195             return "";
196         }
197 
198         return (withWhereKeyword ? " WHERE " : "")
199                 + String.join(mLogicalOperator.opKeyword, mClauses);
200     }
201 }
202