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