001package io.ebean.text;
002
003import io.ebean.FetchPath;
004import io.ebean.Query;
005import io.ebean.util.SplitName;
006
007import java.util.Collection;
008import java.util.Iterator;
009import java.util.LinkedHashMap;
010import java.util.LinkedHashSet;
011import java.util.Map;
012import java.util.Map.Entry;
013import java.util.Set;
014
015/**
016 * This is a Tree like structure of paths and properties that can be used for
017 * defining which parts of an object graph to render in JSON or XML, and can
018 * also be used to define which parts to select and fetch for an ORM query.
019 * <p>
020 * It provides a way of parsing a string representation of nested path
021 * properties and applying that to both what to fetch (ORM query) and what to
022 * render (JAX-RS JSON / XML).
023 * </p>
024 */
025public class PathProperties implements FetchPath {
026
027  private final Map<String, Props> pathMap;
028
029  private final Props rootProps;
030
031  /**
032   * Parse and return a PathProperties from nested string format like
033   * (a,b,c(d,e),f(g)) where "c" is a path containing "d" and "e" and "f" is a
034   * path containing "g" and the root path contains "a","b","c" and "f".
035   */
036  public static PathProperties parse(String source) {
037    return PathPropertiesParser.parse(source);
038  }
039
040  /**
041   * Construct an empty PathProperties.
042   */
043  public PathProperties() {
044    this.rootProps = new Props(this, null, null);
045    this.pathMap = new LinkedHashMap<>();
046    this.pathMap.put(null, rootProps);
047  }
048
049  @Override
050  public String toString() {
051    return pathMap.toString();
052  }
053
054  /**
055   * Return true if the path is defined and has properties.
056   */
057  @Override
058  public boolean hasPath(String path) {
059    Props props = pathMap.get(path);
060    return props != null && !props.isEmpty();
061  }
062
063  /**
064   * Get the properties for a given path.
065   */
066  @Override
067  public Set<String> getProperties(String path) {
068    Props props = pathMap.get(path);
069    return props == null ? null : props.getProperties();
070  }
071
072  public void addToPath(String path, String property) {
073    getProps(path).getProperties().add(property);
074  }
075
076  public void addNested(String prefix, PathProperties pathProps) {
077
078    for (Entry<String, Props> entry : pathProps.pathMap.entrySet()) {
079
080      String path = pathAdd(prefix, entry.getKey());
081      String[] split = SplitName.split(path);
082      getProps(split[0]).addProperty(split[1]);
083      getProps(path).addProps(entry.getValue());
084    }
085  }
086
087  private String pathAdd(String prefix, String key) {
088    return key == null ? prefix : prefix + "." + key;
089  }
090
091  Props getProps(String path) {
092    return pathMap.computeIfAbsent(path, p -> new Props(this, null, p));
093  }
094
095  public Collection<Props> getPathProps() {
096    return pathMap.values();
097  }
098
099  /**
100   * Apply these path properties as fetch paths to the query.
101   */
102  @Override
103  public <T> void apply(Query<T> query) {
104
105    for (Entry<String, Props> entry : pathMap.entrySet()) {
106      String path = entry.getKey();
107      String props = entry.getValue().getPropertiesAsString();
108
109      if (path == null || path.isEmpty()) {
110        query.select(props);
111      } else {
112        query.fetch(path, props);
113      }
114    }
115  }
116
117  protected Props getRootProperties() {
118    return rootProps;
119  }
120
121  /**
122   * Return true if the property (dot notation) is included in the PathProperties.
123   */
124  public boolean includesProperty(String name) {
125
126    String[] split = SplitName.split(name);
127    Props props = pathMap.get(split[0]);
128    return (props != null && props.includes(split[1]));
129  }
130
131  /**
132   * Return true if the property is included using a prefix.
133   */
134  public boolean includesProperty(String prefix, String name) {
135    return includesProperty(SplitName.add(prefix, name));
136  }
137
138  /**
139   * Return true if the fetch path is included in the PathProperties.
140   * <p>
141   * The fetch path is a OneToMany or ManyToMany path in dot notation.
142   * </p>
143   */
144  public boolean includesPath(String path) {
145    return pathMap.containsKey(path);
146  }
147
148  /**
149   * Return true if the path is included using a prefix.
150   */
151  public boolean includesPath(String prefix, String name) {
152    return includesPath(SplitName.add(prefix, name));
153  }
154
155  public static class Props {
156
157    private final PathProperties owner;
158
159    private final String parentPath;
160    private final String path;
161
162    private final LinkedHashSet<String> propSet;
163
164    private Props(PathProperties owner, String parentPath, String path, LinkedHashSet<String> propSet) {
165      this.owner = owner;
166      this.path = path;
167      this.parentPath = parentPath;
168      this.propSet = propSet;
169    }
170
171    private Props(PathProperties owner, String parentPath, String path) {
172      this(owner, parentPath, path, new LinkedHashSet<>());
173    }
174
175    public String getPath() {
176      return path;
177    }
178
179    @Override
180    public String toString() {
181      return propSet.toString();
182    }
183
184    public boolean isEmpty() {
185      return propSet.isEmpty();
186    }
187
188    /**
189     * Return the properties for this property set.
190     */
191    public LinkedHashSet<String> getProperties() {
192      return propSet;
193    }
194
195    /**
196     * Return the properties as a comma delimited string.
197     */
198    public String getPropertiesAsString() {
199
200      StringBuilder sb = new StringBuilder();
201
202      Iterator<String> it = propSet.iterator();
203      boolean hasNext = it.hasNext();
204      while (hasNext) {
205        sb.append(it.next());
206        hasNext = it.hasNext();
207        if (hasNext) {
208          sb.append(",");
209        }
210      }
211      return sb.toString();
212    }
213
214    /**
215     * Return the parent path
216     */
217    protected Props getParent() {
218      return owner.pathMap.get(parentPath);
219    }
220
221    /**
222     * Add a child Property set.
223     */
224    protected Props addChild(String subPath) {
225
226      subPath = subPath.trim();
227      addProperty(subPath);
228
229      // build the subPath
230      String fullPath = path == null ? subPath : path + "." + subPath;
231      Props nested = new Props(owner, path, fullPath);
232      owner.pathMap.put(fullPath, nested);
233      return nested;
234    }
235
236    /**
237     * Add a properties to include for this path.
238     */
239    protected void addProperty(String property) {
240      propSet.add(property.trim());
241    }
242
243    private void addProps(Props value) {
244      propSet.addAll(value.propSet);
245    }
246
247    private boolean includes(String prop) {
248      return propSet.isEmpty() || propSet.contains(prop) || propSet.contains("*");
249    }
250  }
251
252}