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}