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 Set<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, Set<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 Set<String> propSet; 164 165 private Props(PathProperties owner, String parentPath, String path, Set<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 Set<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}