001/* 002 * (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Florent Guillaume 018 */ 019 020package org.nuxeo.ecm.core.storage.sql.db; 021 022import java.io.Serializable; 023import java.sql.Connection; 024import java.sql.DriverManager; 025import java.sql.PreparedStatement; 026import java.sql.ResultSet; 027import java.sql.SQLException; 028import java.util.ArrayList; 029import java.util.Collection; 030import java.util.Collections; 031import java.util.HashSet; 032import java.util.List; 033import java.util.Set; 034import java.util.regex.Pattern; 035 036/** 037 * Functions used as stored procedures for Derby and H2. 038 * 039 * @author Florent Guillaume 040 */ 041public class EmbeddedFunctions { 042 043 // for debug 044 private static boolean isLogEnabled() { 045 return false; 046 // return log.isTraceEnabled(); 047 } 048 049 // for debug 050 private static void logDebug(String message) { 051 // log.trace(message); 052 } 053 054 /** 055 * Checks if an id is a (strict) descendant of a given base id. 056 * 057 * @param id the id to check for 058 * @param baseId the base id 059 */ 060 public static boolean isInTree(Serializable id, Serializable baseId) throws SQLException { 061 try (Connection conn = DriverManager.getConnection("jdbc:default:connection")) { 062 return isInTree(conn, id, baseId); 063 } 064 } 065 066 /** 067 * Checks if an id is a (strict) descendant of a given base id. 068 * 069 * @param conn the connection to the database 070 * @param id the id to check for 071 * @param baseId the base id 072 */ 073 public static boolean isInTree(Connection conn, Serializable id, Serializable baseId) throws SQLException { 074 if (baseId == null || id == null || baseId.equals(id)) { 075 // containment check is strict 076 return false; 077 } 078 try (PreparedStatement ps = conn.prepareStatement("SELECT PARENTID, ISPROPERTY FROM HIERARCHY WHERE ID = ?")) { 079 do { 080 ps.setObject(1, id); 081 try (ResultSet rs = ps.executeQuery()) { 082 if (!rs.next()) { 083 // no such id 084 return false; 085 } 086 if (id instanceof String) { 087 id = rs.getString(1); 088 } else { 089 id = Long.valueOf(rs.getLong(1)); 090 } 091 if (rs.wasNull()) { 092 id = null; 093 } 094 boolean isProperty = rs.getBoolean(2); 095 if (isProperty) { 096 // a complex property is never in-tree 097 return false; 098 } 099 } 100 if (baseId.equals(id)) { 101 // found a match 102 return true; 103 } 104 } while (id != null); 105 // got to the root 106 return false; 107 } 108 } 109 110 /** 111 * Checks if access to a document is allowed. 112 * <p> 113 * This implements in SQL the ACL-based security policy logic. 114 * 115 * @param id the id of the document 116 * @param principals the allowed identities 117 * @param permissions the allowed permissions 118 */ 119 public static boolean isAccessAllowed(Serializable id, Set<String> principals, Set<String> permissions) 120 throws SQLException { 121 try (Connection conn = DriverManager.getConnection("jdbc:default:connection")) { 122 return isAccessAllowed(conn, id, principals, permissions); 123 } 124 } 125 126 /** 127 * Checks if access to a document is allowed. 128 * <p> 129 * This implements in SQL the ACL-based security policy logic. 130 * 131 * @param conn the database connection 132 * @param id the id of the document 133 * @param principals the allowed identities 134 * @param permissions the allowed permissions 135 */ 136 public static boolean isAccessAllowed(Connection conn, Serializable id, Set<String> principals, 137 Set<String> permissions) throws SQLException { 138 if (isLogEnabled()) { 139 logDebug("isAccessAllowed " + id + " " + principals + " " + permissions); 140 } 141 try (PreparedStatement ps1 = conn.prepareStatement( 142 "SELECT \"GRANT\", \"PERMISSION\", \"USER\" FROM \"ACLS\" WHERE ID = ? AND (STATUS IS NULL OR STATUS = 1) ORDER BY POS"); 143 PreparedStatement ps2 = conn.prepareStatement("SELECT PARENTID FROM HIERARCHY WHERE ID = ?")) { 144 boolean first = true; 145 do { 146 /* 147 * Check permissions at this level. 148 */ 149 ps1.setObject(1, id); 150 try (ResultSet rs = ps1.executeQuery()) { 151 while (rs.next()) { 152 boolean grant = rs.getShort(1) != 0; 153 String permission = rs.getString(2); 154 String user = rs.getString(3); 155 if (isLogEnabled()) { 156 logDebug(" -> " + user + " " + permission + " " + grant); 157 } 158 if (principals.contains(user) && permissions.contains(permission)) { 159 if (isLogEnabled()) { 160 logDebug(" => " + grant); 161 } 162 return grant; 163 } 164 } 165 } 166 /* 167 * Nothing conclusive found, repeat on the parent. 168 */ 169 ps2.setObject(1, id); 170 Serializable newId; 171 try (ResultSet rs = ps2.executeQuery()) { 172 if (rs.next()) { 173 newId = (Serializable) rs.getObject(1); 174 if (rs.wasNull()) { 175 newId = null; 176 } 177 } else { 178 // no such id 179 newId = null; 180 } 181 } 182 if (first && newId == null) { 183 // there is no parent for the first level 184 // we may have a version on our hands, find the live doc 185 try (PreparedStatement ps3 = conn.prepareStatement( 186 "SELECT VERSIONABLEID FROM VERSIONS WHERE ID = ?")) { 187 ps3.setObject(1, id); 188 try (ResultSet rs = ps3.executeQuery()) { 189 if (rs.next()) { 190 newId = (Serializable) rs.getObject(1); 191 if (rs.wasNull()) { 192 newId = null; 193 } 194 } else { 195 // no such id 196 newId = null; 197 } 198 } 199 } 200 } 201 first = false; 202 id = newId; 203 } while (id != null); 204 /* 205 * We reached the root, deny access. 206 */ 207 if (isLogEnabled()) { 208 logDebug(" => false (root)"); 209 } 210 return false; 211 } 212 } 213 214 /** 215 * Extracts the words from a string for simple fulltext indexing. 216 * 217 * @param string1 the first string 218 * @param string2 the second string 219 * @return a string with extracted words 220 */ 221 public static String parseFullText(String string1, String string2) { 222 Set<String> set = new HashSet<String>(); 223 set.addAll(parseFullText(string1)); 224 set.addAll(parseFullText(string2)); 225 List<String> words = new ArrayList<String>(set); 226 Collections.sort(words); 227 return join(words, ' '); 228 } 229 230 protected static Set<String> parseFullText(String string) { 231 if (string == null) { 232 return Collections.emptySet(); 233 } 234 Set<String> set = new HashSet<String>(); 235 for (String word : wordPattern.split(string)) { 236 String w = parseWord(word); 237 if (w != null) { 238 set.add(w); 239 } 240 } 241 return set; 242 } 243 244 /** 245 * Checks if the passed query expression matches the fulltext. 246 * 247 * @param fulltext the fulltext, space-separated words 248 * @param query a list of space-separated words 249 * @return {@code true} if all the words are in the fulltext 250 */ 251 protected static boolean matchesFullText(String fulltext, String query) { 252 if (fulltext == null || query == null) { 253 return false; 254 } 255 Set<String> words = split(query.toLowerCase(), ' '); 256 Set<String> filtered = new HashSet<String>(); 257 for (String word : words) { 258 if (!wordPattern.matcher(word).matches()) { 259 filtered.add(word); 260 } 261 } 262 words = filtered; 263 if (words.isEmpty()) { 264 return false; 265 } 266 Set<String> fulltextWords = split(fulltext.toLowerCase(), ' '); 267 for (String word : words) { 268 if (word.endsWith("*") || word.endsWith("%")) { 269 // prefix match 270 String prefix = word.substring(0, word.length() - 2); 271 boolean match = false; 272 for (String candidate : fulltextWords) { 273 if (candidate.startsWith(prefix)) { 274 match = true; 275 break; 276 } 277 } 278 if (!match) { 279 return false; 280 } 281 } else { 282 if (!fulltextWords.contains(word)) { 283 return false; 284 } 285 } 286 } 287 return true; 288 } 289 290 // ----- simple parsing, don't try to be exhaustive ----- 291 292 private static final Pattern wordPattern = Pattern.compile("[\\s\\p{Punct}]+"); 293 294 private static final String UNACCENTED = "aaaaaaaceeeeiiii\u00f0nooooo\u00f7ouuuuy\u00fey"; 295 296 private static final String STOPWORDS = "a an are and as at be by for from how " 297 + "i in is it of on or that the this to was what when where who will with " 298 + "car donc est il ils je la le les mais ni nous or ou pour tu un une vous " + "www com net org"; 299 300 private static final Set<String> stopWords = new HashSet<String>(split(STOPWORDS, ' ')); 301 302 public static final String parseWord(String string) { 303 int len = string.length(); 304 if (len < 3) { 305 return null; 306 } 307 StringBuilder buf = new StringBuilder(len); 308 for (int i = 0; i < len; i++) { 309 char c = Character.toLowerCase(string.charAt(i)); 310 if (c == '\u00e6') { 311 buf.append("ae"); 312 } else if (c >= '\u00e0' && c <= '\u00ff') { 313 buf.append(UNACCENTED.charAt((c) - 0xe0)); 314 } else if (c == '\u0153') { 315 buf.append("oe"); 316 } else { 317 buf.append(c); 318 } 319 } 320 // simple heuristic to remove plurals 321 int l = buf.length(); 322 if (l > 3 && buf.charAt(l - 1) == 's') { 323 buf.setLength(l - 1); 324 } 325 String word = buf.toString(); 326 if (stopWords.contains(word)) { 327 return null; 328 } 329 return word; 330 } 331 332 // ----- utility functions ----- 333 334 public static Set<String> split(String string) { 335 return split(string, '|'); 336 } 337 338 public static Set<String> split(String string, char sep) { 339 int len = string.length(); 340 if (len == 0) { 341 return Collections.emptySet(); 342 } 343 int end = string.indexOf(sep); 344 if (end == -1) { 345 return Collections.singleton(string); 346 } 347 Set<String> set = new HashSet<String>(); 348 int start = 0; 349 do { 350 String segment = string.substring(start, end); 351 set.add(segment); 352 start = end + 1; 353 end = string.indexOf(sep, start); 354 } while (end != -1); 355 if (start < len) { 356 set.add(string.substring(start)); 357 } else { 358 set.add(""); 359 } 360 return set; 361 } 362 363 private static final String join(Collection<String> strings, char sep) { 364 if (strings == null || strings.isEmpty()) { 365 return ""; 366 } 367 int size = 0; 368 for (String word : strings) { 369 size += word.length() + 1; 370 } 371 StringBuilder buf = new StringBuilder(size); 372 for (String word : strings) { 373 buf.append(word); 374 buf.append(sep); 375 } 376 buf.setLength(size - 1); 377 return buf.toString(); 378 } 379 380}