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}