• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 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 android.app.appsearch;
18 
19 import android.annotation.SuppressLint;
20 import android.app.appsearch.checker.initialization.qual.UnderInitialization;
21 import android.app.appsearch.checker.nullness.qual.RequiresNonNull;
22 
23 import org.jspecify.annotations.NonNull;
24 import org.jspecify.annotations.Nullable;
25 
26 import java.util.ArrayList;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.Objects;
30 
31 /**
32  * Represents a property path returned from searching the AppSearch Database.
33  *
34  * <p>One of the use cases for this class is when searching the AppSearch Database for the snippet
35  * matching use case. In this case you will get back {@link SearchResult.MatchInfo} objects that
36  * contain a property path signifying the location of a match within the database. This is a string
37  * that may look something like "foo.bar[0]". {@link PropertyPath} parses this string and breaks it
38  * up into a List of {@link PathSegment}s. These may represent either a property or a property and a
39  * 0-based index into the property. For instance, "foo.bar[1]" would be parsed into a {@link
40  * PathSegment} with a property name of foo and a {@link PathSegment} with a property name of bar
41  * and an index of 1. This allows for easier manipulation of the property path.
42  *
43  * <p>This class won't perform any retrievals, it will only parse the path string. As such, it may
44  * not necessarily refer to a valid path in the database.
45  *
46  * @see SearchResult.MatchInfo
47  */
48 public class PropertyPath implements Iterable<PropertyPath.PathSegment> {
49     private final List<PathSegment> mPathList;
50 
51     /**
52      * Constructor directly accepting a path list
53      *
54      * @param pathList a list of PathSegments
55      */
PropertyPath(@onNull List<PathSegment> pathList)56     public PropertyPath(@NonNull List<PathSegment> pathList) {
57         mPathList = new ArrayList<>(pathList);
58     }
59 
60     /**
61      * Constructor that parses a string representing the path to populate a List of PathSegments
62      *
63      * @param path the string to be validated and parsed into PathSegments
64      * @throws IllegalArgumentException when the path is invalid or malformed
65      */
PropertyPath(@onNull String path)66     public PropertyPath(@NonNull String path) {
67         Objects.requireNonNull(path);
68         mPathList = new ArrayList<>();
69         try {
70             recursivePathScan(path);
71         } catch (IllegalArgumentException e) {
72             // Throw the entire path in a new exception, recursivePathScan may only know about part
73             // of the path.
74             throw new IllegalArgumentException(e.getMessage() + ": " + path);
75         }
76     }
77 
78     @RequiresNonNull("mPathList")
recursivePathScan(@nderInitialization PropertyPath this, String path)79     private void recursivePathScan(@UnderInitialization PropertyPath this, String path)
80             throws IllegalArgumentException {
81         // Determine whether the path is just a raw property name with no control characters
82         int controlPos = -1;
83         boolean controlIsIndex = false;
84         for (int i = 0; i < path.length(); i++) {
85             char c = path.charAt(i);
86             if (c == ']') {
87                 throw new IllegalArgumentException("Malformed path (no starting '[')");
88             }
89             if (c == '[' || c == '.') {
90                 controlPos = i;
91                 controlIsIndex = c == '[';
92                 break;
93             }
94         }
95 
96         if (controlPos == 0 || path.isEmpty()) {
97             throw new IllegalArgumentException("Malformed path (blank property name)");
98         }
99 
100         // If the path has no further elements, we're done.
101         if (controlPos == -1) {
102             // The property's cardinality may be REPEATED, but this path isn't indexing into it
103             mPathList.add(new PathSegment(path, PathSegment.NON_REPEATED_CARDINALITY));
104             return;
105         }
106 
107         String remainingPath;
108         if (!controlIsIndex) {
109             String propertyName = path.substring(0, controlPos);
110             // Remaining path is everything after the .
111             remainingPath = path.substring(controlPos + 1);
112             mPathList.add(new PathSegment(propertyName, PathSegment.NON_REPEATED_CARDINALITY));
113         } else {
114             remainingPath = consumePropertyWithIndex(path, controlPos);
115             // No more path remains, we have nothing to recurse into
116             if (remainingPath == null) {
117                 return;
118             }
119         }
120 
121         // More of the path remains; recursively evaluate it
122         recursivePathScan(remainingPath);
123     }
124 
125     /**
126      * Helper method to parse the parts of the path String that signify indices with square brackets
127      *
128      * <p>For example, when parsing the path "foo[3]", this will be used to parse the "[3]" part of
129      * the path to determine the index into the preceding "foo" property.
130      *
131      * @param path the string we are parsing
132      * @param controlPos the position of the start bracket
133      * @return the rest of the path after the end brackets, or null if there is nothing after them
134      */
135     @RequiresNonNull("mPathList")
consumePropertyWithIndex( @nderInitialization PropertyPath this, @NonNull String path, int controlPos)136     private @Nullable String consumePropertyWithIndex(
137             @UnderInitialization PropertyPath this, @NonNull String path, int controlPos) {
138         Objects.requireNonNull(path);
139         String propertyName = path.substring(0, controlPos);
140         int endBracketIdx = path.indexOf(']', controlPos);
141         if (endBracketIdx == -1) {
142             throw new IllegalArgumentException("Malformed path (no ending ']')");
143         }
144         if (endBracketIdx + 1 < path.length() && path.charAt(endBracketIdx + 1) != '.') {
145             throw new IllegalArgumentException("Malformed path (']' not followed by '.'): " + path);
146         }
147         String indexStr = path.substring(controlPos + 1, endBracketIdx);
148         int index;
149         try {
150             index = Integer.parseInt(indexStr);
151         } catch (NumberFormatException e) {
152             throw new IllegalArgumentException(
153                     "Malformed path (\"" + indexStr + "\" as path index)");
154         }
155         if (index < 0) {
156             throw new IllegalArgumentException("Malformed path (path index less than 0)");
157         }
158         mPathList.add(new PathSegment(propertyName, index));
159         // Remaining path is everything after the [n]
160         if (endBracketIdx + 1 < path.length()) {
161             // More path remains, and we've already checked that charAt(endBracketIdx+1) == .
162             return path.substring(endBracketIdx + 2);
163         } else {
164             return null;
165         }
166     }
167 
168     /**
169      * Returns the {@link PathSegment} at a specified index of the PropertyPath.
170      *
171      * <p>Calling {@code get(1)} on a {@link PropertyPath} representing "foo.bar[1]" will return a
172      * {@link PathSegment} representing "bar[1]". {@link PathSegment}s both with and without a
173      * property index of {@link PathSegment#NON_REPEATED_CARDINALITY} are retrieved the same.
174      *
175      * @param index the position into the PropertyPath
176      * @throws ArrayIndexOutOfBoundsException if index is not a valid index in the path list
177      */
178     // Allow use of the Kotlin indexing operator
179     @SuppressWarnings("KotlinOperator")
180     @SuppressLint("KotlinOperator")
get(int index)181     public @NonNull PathSegment get(int index) {
182         return mPathList.get(index);
183     }
184 
185     /**
186      * Returns the number of {@link PathSegment}s in the PropertyPath.
187      *
188      * <p>Paths representing "foo.bar" and "foo[1].bar[1]" will have the same size, as a property
189      * and an index into that property are stored in one {@link PathSegment}.
190      */
size()191     public int size() {
192         return mPathList.size();
193     }
194 
195     /** Returns a valid path string representing this PropertyPath */
196     @Override
toString()197     public @NonNull String toString() {
198         StringBuilder result = new StringBuilder();
199         for (int i = 0; i < mPathList.size(); i++) {
200             result.append(get(i).toString());
201             if (i < mPathList.size() - 1) {
202                 result.append('.');
203             }
204         }
205 
206         return result.toString();
207     }
208 
209     /** Returns an iterator over the PathSegments within the PropertyPath */
210     @Override
iterator()211     public @NonNull Iterator<PathSegment> iterator() {
212         return mPathList.iterator();
213     }
214 
215     @Override
equals(@ullable Object o)216     public boolean equals(@Nullable Object o) {
217         if (this == o) {
218             return true;
219         }
220         if (o == null) {
221             return false;
222         }
223         if (!(o instanceof PropertyPath)) {
224             return false;
225         }
226         PropertyPath that = (PropertyPath) o;
227         return Objects.equals(mPathList, that.mPathList);
228     }
229 
230     @Override
hashCode()231     public int hashCode() {
232         return Objects.hashCode(mPathList);
233     }
234 
235     /**
236      * A segment of a PropertyPath, which includes the name of the property and a 0-based index into
237      * this property.
238      *
239      * <p>If the property index is not set to {@link #NON_REPEATED_CARDINALITY}, this represents a
240      * schema property with the "repeated" cardinality, or a path like "foo[1]". Otherwise, this
241      * represents a schema property that could have any cardinality, or a path like "foo".
242      */
243     public static class PathSegment {
244         /**
245          * A marker variable to signify that a PathSegment represents a schema property that isn't
246          * indexed into. The value is chosen to be invalid if used as an array index.
247          */
248         public static final int NON_REPEATED_CARDINALITY = -1;
249 
250         private final @NonNull String mPropertyName;
251         private final int mPropertyIndex;
252 
253         /**
254          * Creation method that accepts and validates both a property name and the index into the
255          * property.
256          *
257          * <p>The property name may not be blank. It also may not contain square brackets or dots,
258          * as they are control characters in property paths. The index into the property may not be
259          * negative, unless it is {@link #NON_REPEATED_CARDINALITY}, as these are invalid array
260          * indices.
261          *
262          * @param propertyName the name of the property
263          * @param propertyIndex the index into the property
264          * @return A new PathSegment
265          * @throws IllegalArgumentException if the property name or index is invalid.
266          */
create(@onNull String propertyName, int propertyIndex)267         public static @NonNull PathSegment create(@NonNull String propertyName, int propertyIndex) {
268             Objects.requireNonNull(propertyName);
269             // A path may contain control characters, but a PathSegment may not
270             if (propertyName.isEmpty()
271                     || propertyName.contains("[")
272                     || propertyName.contains("]")
273                     || propertyName.contains(".")) {
274                 throw new IllegalArgumentException("Invalid propertyName value:" + propertyName);
275             }
276             // Has to be a positive integer or the special marker
277             if (propertyIndex < 0 && propertyIndex != NON_REPEATED_CARDINALITY) {
278                 throw new IllegalArgumentException("Invalid propertyIndex value:" + propertyIndex);
279             }
280             return new PathSegment(propertyName, propertyIndex);
281         }
282 
283         /**
284          * Creation method that accepts and validates a property name
285          *
286          * <p>The property index is set to {@link #NON_REPEATED_CARDINALITY}
287          *
288          * @param propertyName the name of the property
289          * @return A new PathSegment
290          */
create(@onNull String propertyName)291         public static @NonNull PathSegment create(@NonNull String propertyName) {
292             return create(Objects.requireNonNull(propertyName), NON_REPEATED_CARDINALITY);
293         }
294 
295         /**
296          * Package-private constructor that accepts a property name and an index into the property
297          * without validating either of them
298          *
299          * @param propertyName the name of the property
300          * @param propertyIndex the index into the property
301          */
PathSegment(@onNull String propertyName, int propertyIndex)302         PathSegment(@NonNull String propertyName, int propertyIndex) {
303             mPropertyName = Objects.requireNonNull(propertyName);
304             mPropertyIndex = propertyIndex;
305         }
306 
307         /** Returns the name of the property. */
getPropertyName()308         public @NonNull String getPropertyName() {
309             return mPropertyName;
310         }
311 
312         /**
313          * Returns the index into the property, or {@link #NON_REPEATED_CARDINALITY} if this does
314          * not represent a PathSegment with an index.
315          */
getPropertyIndex()316         public int getPropertyIndex() {
317             return mPropertyIndex;
318         }
319 
320         /** Returns a path representing a PathSegment, either "foo" or "foo[1]" */
321         @Override
toString()322         public @NonNull String toString() {
323             if (mPropertyIndex != NON_REPEATED_CARDINALITY) {
324                 return mPropertyName + "[" + mPropertyIndex + "]";
325             }
326             return mPropertyName;
327         }
328 
329         @Override
equals(@ullable Object o)330         public boolean equals(@Nullable Object o) {
331             if (this == o) {
332                 return true;
333             }
334             if (o == null) {
335                 return false;
336             }
337             if (!(o instanceof PathSegment)) {
338                 return false;
339             }
340             PathSegment that = (PathSegment) o;
341             return mPropertyIndex == that.mPropertyIndex
342                     && mPropertyName.equals(that.mPropertyName);
343         }
344 
345         @Override
hashCode()346         public int hashCode() {
347             return Objects.hash(mPropertyName, mPropertyIndex);
348         }
349     }
350 }
351