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}