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