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}