001package com.avaje.ebean.text;
002
003import com.avaje.ebean.FetchPath;
004import com.avaje.ebean.Query;
005import com.avaje.ebeaninternal.server.query.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<String, Props>();
046    this.pathMap.put(null, rootProps);
047  }
048
049  public String toString() {
050    return pathMap.toString();
051  }
052
053  /**
054   * Return true if the path is defined and has properties.
055   */
056  @Override
057  public boolean hasPath(String path) {
058    Props props = pathMap.get(path);
059    return props != null && !props.isEmpty();
060  }
061
062  /**
063   * Get the properties for a given path.
064   */
065  @Override
066  public Set<String> getProperties(String path) {
067    Props props = pathMap.get(path);
068    return props == null ? null : props.getProperties();
069  }
070
071  public void addToPath(String path, String property) {
072    getProps(path).getProperties().add(property);
073  }
074
075  public void addNested(String prefix, PathProperties pathProps) {
076
077    for (Entry<String, Props> entry : pathProps.pathMap.entrySet()) {
078
079      String path = pathAdd(prefix, entry.getKey());
080      String[] split = SplitName.split(path);
081      getProps(split[0]).addProperty(split[1]);
082      getProps(path).addProps(entry.getValue());
083    }
084  }
085
086  private String pathAdd(String prefix, String key) {
087    return key == null ? prefix : prefix + "." + key;
088  }
089
090  Props getProps(String path) {
091    Props props = pathMap.get(path);
092    if (props == null) {
093      props = new Props(this, null, path);
094      pathMap.put(path, props);
095    }
096    return props;
097  }
098
099  public Collection<Props> getPathProps() {
100    return pathMap.values();
101  }
102
103  /**
104   * Apply these path properties as fetch paths to the query.
105   */
106  public <T> void apply(Query<T> query) {
107
108    for (Entry<String, Props> entry : pathMap.entrySet()) {
109      String path = entry.getKey();
110      String props = entry.getValue().getPropertiesAsString();
111
112      if (path == null || path.isEmpty()) {
113        query.select(props);
114      } else {
115        query.fetch(path, props);
116      }
117    }
118  }
119
120  protected Props getRootProperties() {
121    return rootProps;
122  }
123
124  /**
125   * Return true if the property (dot notation) is included in the PathProperties.
126   */
127  public boolean includesProperty(String name) {
128
129    String[] split = SplitName.split(name);
130    Props props = pathMap.get(split[0]);
131    return (props != null && props.includes(split[1]));
132  }
133
134  /**
135   * Return true if the property is included using a prefix.
136   */
137  public boolean includesProperty(String prefix, String name) {
138    return includesProperty(SplitName.add(prefix, name));
139  }
140
141  /**
142   * Return true if the fetch path is included in the PathProperties.
143   * <p>
144   * The fetch path is a OneToMany or ManyToMany path in dot notation.
145   * </p>
146   */
147  public boolean includesPath(String path) {
148    return pathMap.containsKey(path);
149  }
150
151  /**
152   * Return true if the path is included using a prefix.
153   */
154  public boolean includesPath(String prefix, String name) {
155    return includesPath(SplitName.add(prefix, name));
156  }
157
158  public static class Props {
159
160    private final PathProperties owner;
161
162    private final String parentPath;
163    private final String path;
164
165    private final LinkedHashSet<String> propSet;
166
167    private Props(PathProperties owner, String parentPath, String path, LinkedHashSet<String> propSet) {
168      this.owner = owner;
169      this.path = path;
170      this.parentPath = parentPath;
171      this.propSet = propSet;
172    }
173
174    private Props(PathProperties owner, String parentPath, String path) {
175      this(owner, parentPath, path, new LinkedHashSet<String>());
176    }
177
178    public String getPath() {
179      return path;
180    }
181
182    public String toString() {
183      return propSet.toString();
184    }
185
186    public boolean isEmpty() {
187      return propSet.isEmpty();
188    }
189
190    /**
191     * Return the properties for this property set.
192     */
193    public LinkedHashSet<String> getProperties() {
194      return propSet;
195    }
196
197    /**
198     * Return the properties as a comma delimited string.
199     */
200    public String getPropertiesAsString() {
201
202      StringBuilder sb = new StringBuilder();
203
204      Iterator<String> it = propSet.iterator();
205      boolean hasNext = it.hasNext();
206      while (hasNext) {
207        sb.append(it.next());
208        hasNext = it.hasNext();
209        if (hasNext) {
210          sb.append(",");
211        }
212      }
213      return sb.toString();
214    }
215
216    /**
217     * Return the parent path
218     */
219    protected Props getParent() {
220      return owner.pathMap.get(parentPath);
221    }
222
223    /**
224     * Add a child Property set.
225     */
226    protected Props addChild(String subPath) {
227
228      subPath = subPath.trim();
229      addProperty(subPath);
230
231      // build the subPath
232      String fullPath = path == null ? subPath : path + "." + subPath;
233      Props nested = new Props(owner, path, fullPath);
234      owner.pathMap.put(fullPath, nested);
235      return nested;
236    }
237
238    /**
239     * Add a properties to include for this path.
240     */
241    protected void addProperty(String property) {
242      propSet.add(property.trim());
243    }
244
245    private void addProps(Props value) {
246      propSet.addAll(value.propSet);
247    }
248
249    private boolean includes(String prop) {
250      return propSet.isEmpty() || propSet.contains(prop) || propSet.contains("*");
251    }
252  }
253
254}