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