001/*
002 * (C) Copyright 2011-2018 Nuxeo (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 */
019package org.nuxeo.ecm.platform.relations;
020
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.FileNotFoundException;
024import java.io.FileOutputStream;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.OutputStream;
028import java.io.Serializable;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.Calendar;
032import java.util.Collections;
033import java.util.Date;
034import java.util.HashMap;
035import java.util.LinkedHashSet;
036import java.util.List;
037import java.util.Map;
038import java.util.Map.Entry;
039import java.util.regex.Matcher;
040import java.util.regex.Pattern;
041
042import org.apache.commons.lang3.StringUtils;
043import org.apache.commons.logging.Log;
044import org.apache.commons.logging.LogFactory;
045import org.nuxeo.ecm.core.api.CoreSession;
046import org.nuxeo.ecm.core.api.DocumentModel;
047import org.nuxeo.ecm.core.api.IdRef;
048import org.nuxeo.ecm.core.api.IterableQueryResult;
049import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
050import org.nuxeo.ecm.core.api.repository.RepositoryManager;
051import org.nuxeo.ecm.core.query.sql.NXQL;
052import org.nuxeo.ecm.core.schema.DocumentType;
053import org.nuxeo.ecm.core.schema.SchemaManager;
054import org.nuxeo.ecm.core.schema.types.Type;
055import org.nuxeo.ecm.platform.query.nxql.NXQLQueryBuilder;
056import org.nuxeo.ecm.platform.relations.api.Blank;
057import org.nuxeo.ecm.platform.relations.api.Graph;
058import org.nuxeo.ecm.platform.relations.api.GraphDescription;
059import org.nuxeo.ecm.platform.relations.api.Literal;
060import org.nuxeo.ecm.platform.relations.api.Node;
061import org.nuxeo.ecm.platform.relations.api.NodeType;
062import org.nuxeo.ecm.platform.relations.api.QNameResource;
063import org.nuxeo.ecm.platform.relations.api.QueryResult;
064import org.nuxeo.ecm.platform.relations.api.Resource;
065import org.nuxeo.ecm.platform.relations.api.Statement;
066import org.nuxeo.ecm.platform.relations.api.Subject;
067import org.nuxeo.ecm.platform.relations.api.impl.AbstractNode;
068import org.nuxeo.ecm.platform.relations.api.impl.NodeFactory;
069import org.nuxeo.ecm.platform.relations.api.impl.QueryResultImpl;
070import org.nuxeo.ecm.platform.relations.api.impl.RelationDate;
071import org.nuxeo.ecm.platform.relations.api.impl.StatementImpl;
072import org.nuxeo.ecm.platform.relations.api.util.RelationConstants;
073import org.nuxeo.runtime.api.Framework;
074
075/**
076 * Relation graph implementation delegating to the core.
077 */
078public class CoreGraph implements Graph {
079
080    private static final long serialVersionUID = 1L;
081
082    private static final Log log = LogFactory.getLog(CoreGraph.class);
083
084    public static final String OPTION_DOCTYPE = "doctype";
085
086    public static final String REL_TYPE = "Relation";
087
088    public static final String REL_PREDICATE = "relation:predicate";
089
090    public static final String REL_SOURCE_ID = "relation:source";
091
092    public static final String REL_SOURCE_URI = "relation:sourceUri";
093
094    public static final String REL_TARGET_ID = "relation:target";
095
096    public static final String REL_TARGET_URI = "relation:targetUri";
097
098    public static final String REL_TARGET_STRING = "relation:targetString";
099
100    public static final String DC_CREATED = "dc:created";
101
102    public static final String DC_CREATOR = "dc:creator";
103
104    public static final String DC_MODIFIED = "dc:modified";
105
106    public static final String DC_TITLE = "dc:title";
107
108    public static final String DC_DESCRIPTION = "dc:description";
109
110    // avoid confusion with any legal uri
111    public static final String BLANK_NS = "-:";
112
113    public static final String DOCUMENT_NAMESPACE = RelationConstants.DOCUMENT_NAMESPACE;
114
115    /** Without final slash (compat). */
116    public static final String DOCUMENT_NAMESPACE2 = DOCUMENT_NAMESPACE.substring(0, DOCUMENT_NAMESPACE.length() - 1);
117
118    /** Has no final slash (compat). */
119    public static final String COMMENT_NAMESPACE = "http://www.nuxeo.org/comments/uid";
120
121    public static final String[] DOC_NAMESPACES = { DOCUMENT_NAMESPACE, DOCUMENT_NAMESPACE2, COMMENT_NAMESPACE };
122
123    protected static final List<Statement> EMPTY_STATEMENTS = Collections.emptyList();
124
125    protected static final Statement ALL = new StatementImpl(null, null, null);
126
127    protected CoreSession session;
128
129    protected String name;
130
131    protected String docType = REL_TYPE;
132
133    public Map<String, String> namespaces;
134
135    public List<String> namespaceList = Collections.emptyList();
136
137    /** Only one of those is filled. */
138    protected static class NodeAsString {
139        public String id;
140
141        public String uri;
142
143        public String string;
144    }
145
146    /**
147     * A graph with this base session. An unrestricted session will be opened based on it.
148     */
149    public CoreGraph(CoreSession session) {
150        this.session = session;
151    }
152
153    @Override
154    public void setDescription(GraphDescription graphDescription) {
155        name = graphDescription.getName();
156        setOptions(graphDescription.getOptions());
157        namespaces = graphDescription.getNamespaces();
158        namespaceList = namespaces == null ? Collections.emptyList()
159                : new ArrayList<>(new LinkedHashSet<>(namespaces.values()));
160    }
161
162    protected void setOptions(Map<String, String> options) {
163        for (Entry<String, String> option : options.entrySet()) {
164            String key = option.getKey();
165            String type = option.getValue();
166            if (key.equals(OPTION_DOCTYPE)) {
167                SchemaManager sm = Framework.getService(SchemaManager.class);
168                DocumentType documentType = sm.getDocumentType(type);
169                if (documentType == null) {
170                    throw new IllegalArgumentException("Unknown type: " + type + " for graph: " + name);
171                }
172                Type[] th = documentType.getTypeHierarchy();
173                String baseType = th.length == 0 ? type : th[th.length - 1].getName();
174                if (!REL_TYPE.equals(baseType)) {
175                    throw new IllegalArgumentException("Not a Relation type: " + type + " for graph: " + name);
176                }
177                docType = type;
178            }
179        }
180    }
181
182    @Override
183    public Map<String, String> getNamespaces() {
184        return namespaces;
185    }
186
187    @Override
188    public Long size() {
189        SizeFinder sizeFinder = session == null ? new SizeFinder() : new SizeFinder(session);
190        sizeFinder.runUnrestricted();
191        return Long.valueOf(sizeFinder.size);
192    }
193
194    protected class SizeFinder extends UnrestrictedSessionRunner {
195
196        protected long size;
197
198        protected SizeFinder() {
199            super(getDefaultRepositoryName());
200        }
201
202        protected SizeFinder(CoreSession session) {
203            super(session);
204        }
205
206        @Override
207        public void run() {
208            // TODO could use a COUNT(*) query
209            try (IterableQueryResult it = session.queryAndFetch("SELECT " + NXQL.ECM_UUID + " FROM " + docType,
210                    NXQL.NXQL)) {
211                size = it.size();
212            }
213        }
214    }
215
216    @Override
217    public void clear() {
218        remove(Collections.singletonList(ALL));
219    }
220
221    @Override
222    public void add(Statement statement) {
223        add(Collections.singletonList(statement));
224    }
225
226    @Override
227    public void add(List<Statement> statements) {
228        StatementAdder statementAdder = session == null ? new StatementAdder(statements)
229                : new StatementAdder(statements, session);
230        statementAdder.runUnrestricted();
231    }
232
233    protected class StatementAdder extends UnrestrictedSessionRunner {
234
235        protected List<Statement> statements;
236
237        protected Date now;
238
239        protected StatementAdder(List<Statement> statements) {
240            super(getDefaultRepositoryName());
241            this.statements = statements;
242        }
243
244        protected StatementAdder(List<Statement> statements, CoreSession session) {
245            super(session);
246            this.statements = statements;
247        }
248
249        @Override
250        public void run() {
251            now = new Date();
252            for (Statement statement : statements) {
253                add(statement);
254            }
255            session.save();
256        }
257
258        protected void add(Statement statement) {
259            DocumentModel rel = session.createDocumentModel(null, "relation", docType);
260            rel = setRelationProperties(rel, statement);
261            session.createDocument(rel);
262        }
263
264        protected DocumentModel setRelationProperties(DocumentModel rel, Statement statement) {
265            Resource pred = statement.getPredicate();
266            String predicateUri = pred.getUri();
267            if (predicateUri == null) {
268                throw new IllegalArgumentException("Invalid predicate in statement: " + statement);
269            }
270
271            Subject subject = statement.getSubject();
272            if (subject.isLiteral()) {
273                throw new IllegalArgumentException("Invalid literal subject in statement: " + statement);
274            }
275            NodeAsString source = getNodeAsString(subject);
276
277            Node object = statement.getObject();
278            NodeAsString target = getNodeAsString(object);
279
280            String author = getAuthor(statement);
281            if (author == null) {
282                author = getOriginatingUsername();
283            }
284
285            Date created = getCreationDate(statement);
286            if (created == null) {
287                created = now;
288            }
289
290            Date modified = getModificationDate(statement);
291            if (modified == null) {
292                modified = now;
293            }
294
295            String comment = getComment(statement);
296
297            String title = (source.id != null ? source.id : source.uri) + " "
298                    + predicateUri.substring(predicateUri.lastIndexOf('/') + 1) + " "
299                    + (target.id != null ? target.id : target.uri != null ? target.uri : target.string);
300            int MAX_TITLE = 200;
301            if (title.length() > MAX_TITLE) {
302                title = title.substring(0, MAX_TITLE);
303            }
304
305            rel.setPropertyValue(REL_PREDICATE, predicateUri);
306            if (source.id != null) {
307                rel.setPropertyValue(REL_SOURCE_ID, source.id);
308            } else {
309                rel.setPropertyValue(REL_SOURCE_URI, source.uri);
310            }
311            if (target.id != null) {
312                rel.setPropertyValue(REL_TARGET_ID, target.id);
313            } else if (target.uri != null) {
314                rel.setPropertyValue(REL_TARGET_URI, target.uri);
315            } else {
316                rel.setPropertyValue(REL_TARGET_STRING, target.string);
317            }
318            if (author != null) {
319                // will usually get overwritten by DublinCoreListener
320                // but not in tests
321                rel.setPropertyValue(DC_CREATOR, author);
322            }
323            if (created != null) {
324                // will usually get overwritten by DublinCoreListener
325                // but not in tests
326                rel.setPropertyValue(DC_CREATED, created);
327            }
328            if (modified != null) {
329                // will usually get overwritten by DublinCoreListener
330                // but not in tests
331                rel.setPropertyValue(DC_MODIFIED, modified);
332            }
333            rel.setPropertyValue(DC_TITLE, title); // for debug
334            if (comment != null) {
335                rel.setPropertyValue(DC_DESCRIPTION, comment);
336            }
337            return rel;
338        }
339    }
340
341    @Override
342    public void remove(Statement statement) {
343        remove(Collections.singletonList(statement));
344    }
345
346    @Override
347    public void remove(List<Statement> statements) {
348        StatementRemover statementRemover = session == null ? new StatementRemover(statements)
349                : new StatementRemover(statements, session);
350        statementRemover.runUnrestricted();
351    }
352
353    protected class StatementRemover extends UnrestrictedSessionRunner {
354
355        protected List<Statement> statements;
356
357        protected Date now;
358
359        protected StatementRemover(List<Statement> statements) {
360            super(getDefaultRepositoryName());
361            this.statements = statements;
362        }
363
364        protected StatementRemover(List<Statement> statements, CoreSession session) {
365            super(session);
366            this.statements = statements;
367        }
368
369        @Override
370        public void run() {
371            now = new Date();
372            for (Statement statement : statements) {
373                remove(statement);
374            }
375        }
376
377        protected void remove(Statement statement) {
378            String query = "SELECT " + NXQL.ECM_UUID + " FROM " + docType;
379            query = whereBuilder(query, statement);
380            if (query == null) {
381                return;
382            }
383            try (IterableQueryResult it = session.queryAndFetch(query, NXQL.NXQL)) {
384                for (Map<String, Serializable> map : it) {
385                    String id = (String) map.get(NXQL.ECM_UUID);
386                    session.removeDocument(new IdRef(id));
387                }
388            }
389        }
390    }
391
392    protected class StatementFinder extends UnrestrictedSessionRunner {
393
394        protected List<Statement> statements;
395
396        protected Statement statement;
397
398        protected StatementFinder(Statement statement) {
399            super(getDefaultRepositoryName());
400            this.statement = statement;
401        }
402
403        protected StatementFinder(Statement statement, CoreSession session) {
404            super(session);
405            this.statement = statement;
406        }
407
408        @Override
409        public void run() {
410            String query = "SELECT " + REL_PREDICATE + ", " + REL_SOURCE_ID + ", " + REL_SOURCE_URI + ", "
411                    + REL_TARGET_ID + ", " + REL_TARGET_URI + ", " + REL_TARGET_STRING + ", " + DC_CREATED + ", "
412                    + DC_CREATOR + ", " + DC_MODIFIED + ", " + DC_DESCRIPTION + " FROM " + docType;
413            query = whereBuilder(query, statement);
414            if (query == null) {
415                statements = EMPTY_STATEMENTS;
416                return;
417            }
418            statements = new ArrayList<>();
419            try (IterableQueryResult it = session.queryAndFetch(query, NXQL.NXQL)) {
420                for (Map<String, Serializable> map : it) {
421                    String pred = (String) map.get(REL_PREDICATE);
422                    String source = (String) map.get(REL_SOURCE_ID);
423                    String sourceUri = (String) map.get(REL_SOURCE_URI);
424                    String target = (String) map.get(REL_TARGET_ID);
425                    String targetUri = (String) map.get(REL_TARGET_URI);
426                    String targetString = (String) map.get(REL_TARGET_STRING);
427                    Calendar created = (Calendar) map.get(DC_CREATED);
428                    String creator = (String) map.get(DC_CREATOR);
429                    Calendar modified = (Calendar) map.get(DC_MODIFIED);
430                    String comment = (String) map.get(DC_DESCRIPTION);
431
432                    Resource predicate = NodeFactory.createResource(pred);
433                    Node subject;
434                    if (source != null) {
435                        subject = createId(source);
436                    } else {
437                        subject = createUri(sourceUri);
438                    }
439                    Node object;
440                    if (target != null) {
441                        object = createId(target);
442                    } else if (targetUri != null) {
443                        object = createUri(targetUri);
444                    } else {
445                        object = NodeFactory.createLiteral(targetString);
446                    }
447                    Statement statement = new StatementImpl(subject, predicate, object);
448                    setCreationDate(statement, created);
449                    setAuthor(statement, creator);
450                    setModificationDate(statement, modified);
451                    setComment(statement, comment);
452                    statements.add(statement);
453                }
454            }
455        }
456
457        protected QNameResource createId(String id) {
458            return NodeFactory.createQNameResource(DOCUMENT_NAMESPACE, session.getRepositoryName() + '/' + id);
459        }
460
461        protected Node createUri(String uri) {
462            if (uri.startsWith(BLANK_NS)) {
463                // skolemization
464                String id = uri.substring(BLANK_NS.length());
465                return NodeFactory.createBlank(id.isEmpty() ? null : id);
466            } else {
467                for (String ns : namespaceList) {
468                    if (uri.startsWith(ns)) {
469                        String localName = uri.substring(ns.length());
470                        return NodeFactory.createQNameResource(ns, localName);
471                    }
472                }
473                return NodeFactory.createResource(uri);
474            }
475        }
476
477    }
478
479    @Override
480    public List<Statement> getStatements() {
481        return getStatements(ALL);
482    }
483
484    @Override
485    public List<Statement> getStatements(Node subject, Node predicate, Node object) {
486        return getStatements(new StatementImpl(subject, predicate, object));
487    }
488
489    @Override
490    public List<Statement> getStatements(Statement statement) {
491        StatementFinder statementFinder = session == null ? new StatementFinder(statement)
492                : new StatementFinder(statement, session);
493        statementFinder.runUnrestricted();
494        return statementFinder.statements;
495    }
496
497    @Override
498    public List<Node> getSubjects(Node predicate, Node object) {
499        List<Statement> statements = getStatements(new StatementImpl(null, predicate, object));
500        List<Node> nodes = new ArrayList<>(statements.size());
501        for (Statement statement : statements) {
502            nodes.add(statement.getSubject());
503        }
504        return nodes;
505    }
506
507    @Override
508    public List<Node> getPredicates(Node subject, Node object) {
509        List<Statement> statements = getStatements(new StatementImpl(subject, null, object));
510        List<Node> nodes = new ArrayList<>(statements.size());
511        for (Statement statement : statements) {
512            nodes.add(statement.getPredicate());
513        }
514        return nodes;
515    }
516
517    @Override
518    public List<Node> getObjects(Node subject, Node predicate) {
519        List<Statement> statements = getStatements(new StatementImpl(subject, predicate, null));
520        List<Node> nodes = new ArrayList<>(statements.size());
521        for (Statement statement : statements) {
522            nodes.add(statement.getObject());
523        }
524        return nodes;
525    }
526
527    @Override
528    public boolean hasStatement(Statement statement) {
529        if (statement == null) {
530            return false;
531        }
532        // could be optimized in the null/blank case, but this method seems
533        // unused
534        return !getStatements(statement).isEmpty();
535    }
536
537    @Override
538    public boolean hasResource(Resource resource) {
539        if (resource == null) {
540            return false;
541        }
542        ResourceFinder resourceFinder = session == null ? new ResourceFinder(resource)
543                : new ResourceFinder(resource, session);
544        resourceFinder.runUnrestricted();
545        return resourceFinder.found;
546    }
547
548    protected class ResourceFinder extends UnrestrictedSessionRunner {
549
550        protected boolean found;
551
552        protected Resource resource;
553
554        protected ResourceFinder(Resource resource) {
555            super(getDefaultRepositoryName());
556            this.resource = resource;
557        }
558
559        protected ResourceFinder(Resource resource, CoreSession session) {
560            super(session);
561            this.resource = resource;
562        }
563
564        @Override
565        public void run() {
566            String query = "SELECT " + NXQL.ECM_UUID + " FROM " + docType;
567            query = whereAnyBuilder(query, resource);
568            try (IterableQueryResult it = session.queryAndFetch(query, NXQL.NXQL)) {
569                found = it.iterator().hasNext();
570            }
571        }
572
573        protected String whereAnyBuilder(String query, Resource resource) {
574            List<Object> params = new ArrayList<>(3);
575            List<String> clauses = new ArrayList<>(3);
576
577            NodeAsString nas = getNodeAsString(resource);
578            if (nas.id != null) {
579                // don't allow predicates that are actually doc ids
580                clauses.add(REL_SOURCE_ID + " = ?");
581                params.add(nas.id);
582                clauses.add(REL_TARGET_URI + " = ?");
583                params.add(DOCUMENT_NAMESPACE + session.getRepositoryName() + '/' + nas.id);
584            } else if (nas.uri != null) {
585                for (String ns : DOC_NAMESPACES) {
586                    if (nas.uri.startsWith(ns)) {
587                        String id = localNameToId(nas.uri.substring(ns.length()));
588                        clauses.add(REL_SOURCE_ID + " = ?");
589                        params.add(id);
590                        break;
591                    }
592                }
593                clauses.add(REL_SOURCE_URI + " = ?");
594                params.add(nas.uri);
595                clauses.add(REL_TARGET_URI + " = ?");
596                params.add(nas.uri);
597                clauses.add(REL_PREDICATE + " = ?");
598                params.add(nas.uri);
599            }
600            query += " WHERE " + StringUtils.join(clauses, " OR ");
601            query = NXQLQueryBuilder.getQuery(query, params.toArray(), true, true, null);
602            return query;
603        }
604    }
605
606    public static final Pattern SPARQL_SPO_PO = Pattern.compile(
607            "SELECT \\?s \\?p \\?o WHERE \\{ \\?s \\?p \\?o . \\?s <(.*)> <(.*)> . \\}");
608
609    public static final Pattern SPARQL_PO_S = Pattern.compile("SELECT \\?p \\?o WHERE \\{ <(.*)> \\?p \\?o \\}");
610
611    @Override
612    public QueryResult query(String queryString, String language, String baseURI) {
613        // language is ignored, assume SPARQL
614        Matcher matcher = SPARQL_SPO_PO.matcher(queryString);
615        if (matcher.matches()) {
616            Node predicate = NodeFactory.createResource(matcher.group(1));
617            Node object = NodeFactory.createResource(matcher.group(2));
618            // find subjects with this predicate and object
619            List<Node> subjects = getSubjects(predicate, object);
620            List<Map<String, Node>> results = new ArrayList<>();
621            if (!subjects.isEmpty()) {
622                // find all statements with these subjects
623                List<Statement> statements = getStatements(new Subjects(subjects), null, null);
624                for (Statement st : statements) {
625                    Map<String, Node> map = new HashMap<>();
626                    map.put("s", st.getSubject());
627                    map.put("p", st.getPredicate());
628                    map.put("o", st.getObject());
629                    results.add(map);
630                }
631            }
632            return new QueryResultImpl(Integer.valueOf(results.size()), Arrays.asList("s", "p", "o"), results);
633        }
634        matcher = SPARQL_PO_S.matcher(queryString);
635        if (matcher.matches()) {
636            Node subject = NodeFactory.createResource(matcher.group(1));
637            // find predicates and objects with this subject
638            List<Statement> statements = getStatements(new StatementImpl(subject, null, null));
639            List<Map<String, Node>> results = new ArrayList<>();
640            for (Statement st : statements) {
641                Map<String, Node> map = new HashMap<>();
642                map.put("p", st.getPredicate());
643                map.put("o", st.getObject());
644                results.add(map);
645            }
646            return new QueryResultImpl(Integer.valueOf(results.size()), Arrays.asList("p", "o"), results);
647        }
648        throw new UnsupportedOperationException("Cannot parse query: " + queryString);
649    }
650
651    public static final Pattern SPARQL_S_PO = Pattern.compile("SELECT \\?s WHERE \\{ \\?s <(.*)> <(.*)> \\}");
652
653    @Override
654    public int queryCount(String queryString, String language, String baseURI) {
655        // language is ignored, assume SPARQL
656        Matcher matcher = SPARQL_S_PO.matcher(queryString);
657        if (matcher.matches()) {
658            Node predicate = NodeFactory.createResource(matcher.group(1));
659            Node object = NodeFactory.createResource(matcher.group(2));
660            List<Node> subjects = getSubjects(predicate, object);
661            return subjects.size();
662        }
663        throw new UnsupportedOperationException("Cannot parse query: " + queryString);
664    }
665
666    @Override
667    public boolean read(String path, String lang, String base) {
668        InputStream in = null;
669        try {
670            in = new FileInputStream(path);
671            return read(in, lang, base);
672        } catch (FileNotFoundException e) {
673            throw new RuntimeException(e);
674        } finally {
675            if (in != null) {
676                try {
677                    in.close();
678                } catch (IOException e) {
679                    log.error(e);
680                }
681            }
682        }
683    }
684
685    @Override
686    public boolean write(String path, String lang, String base) {
687        OutputStream out = null;
688        try {
689            out = new FileOutputStream(new File(path));
690            return write(out, lang, base);
691        } catch (FileNotFoundException e) {
692            throw new RuntimeException(e);
693        } finally {
694            if (out != null) {
695                try {
696                    out.close();
697                } catch (IOException e) {
698                    log.error(e);
699                }
700            }
701        }
702    }
703
704    @Override
705    public boolean read(InputStream in, String lang, String base) {
706        throw new UnsupportedOperationException();
707    }
708
709    @Override
710    public boolean write(OutputStream out, String lang, String base) {
711        throw new UnsupportedOperationException();
712    }
713
714    protected static String getDefaultRepositoryName() {
715        return Framework.getService(RepositoryManager.class).getDefaultRepositoryName();
716    }
717
718    /** Fake Node type used to pass down multiple nodes into whereBuilder. */
719    public static class Subjects extends AbstractNode implements Subject {
720
721        private static final long serialVersionUID = 1L;
722
723        protected List<Node> nodes;
724
725        public Subjects(List<Node> nodes) {
726            this.nodes = nodes;
727        }
728
729        public List<Node> getNodes() {
730            return nodes;
731        }
732
733        @Override
734        public NodeType getNodeType() {
735            return null;
736        }
737    }
738
739    protected String whereBuilder(String query, Statement statement) {
740        List<Object> params = new ArrayList<>(3);
741        List<String> clauses = new ArrayList<>(3);
742
743        Resource p = statement.getPredicate();
744        if (p != null) {
745            NodeAsString pn = getNodeAsString(p);
746            if (pn.uri == null) {
747                return null;
748            }
749            clauses.add(REL_PREDICATE + " = ?");
750            params.add(pn.uri);
751        }
752
753        Node s = statement.getSubject();
754        if (s != null) {
755            if (s instanceof Subjects) {
756                List<Node> subjects = ((Subjects) s).getNodes();
757                if (subjects.isEmpty()) {
758                    throw new UnsupportedOperationException("empty subjects");
759                }
760                StringBuilder buf = new StringBuilder(REL_SOURCE_URI);
761                buf.append(" IN (");
762                for (Node sub : subjects) {
763                    NodeAsString sn = getNodeAsString(sub);
764                    if (sn.id != null) {
765                        throw new UnsupportedOperationException("subjects ListNode with id instead of uri" + subjects);
766                    }
767                    buf.append("?, ");
768                    params.add(sn.uri);
769                }
770                buf.setLength(buf.length() - 2); // remove last comma/space
771                buf.append(")");
772                clauses.add(buf.toString());
773            } else {
774                NodeAsString sn = getNodeAsString(s);
775                if (sn.id != null) {
776                    clauses.add(REL_SOURCE_ID + " = ?");
777                    params.add(sn.id);
778                } else {
779                    clauses.add(REL_SOURCE_URI + " = ?");
780                    params.add(sn.uri);
781                }
782
783            }
784        }
785
786        Node o = statement.getObject();
787        if (o != null) {
788            NodeAsString on = getNodeAsString(o);
789            if (on.id != null) {
790                clauses.add(REL_TARGET_ID + " = ?");
791                params.add(on.id);
792            } else if (on.uri != null) {
793                clauses.add(REL_TARGET_URI + " = ?");
794                params.add(on.uri);
795            } else {
796                clauses.add(REL_TARGET_STRING + " = ?");
797                params.add(on.string);
798            }
799        }
800
801        if (!clauses.isEmpty()) {
802            query += " WHERE " + StringUtils.join(clauses, " AND ");
803            query = NXQLQueryBuilder.getQuery(query, params.toArray(), true, true, null);
804        }
805        return query;
806    }
807
808    protected static NodeAsString getNodeAsString(Node node) {
809        NodeAsString nas = new NodeAsString();
810        if (node.isBlank()) {
811            // skolemization
812            String id = ((Blank) node).getId();
813            nas.uri = BLANK_NS + (id == null ? "" : id);
814        } else if (node.isLiteral()) {
815            nas.string = ((Literal) node).getValue();
816        } else if (node.isQNameResource()) {
817            String ns = ((QNameResource) node).getNamespace();
818            if (DOCUMENT_NAMESPACE.equals(ns) || DOCUMENT_NAMESPACE2.equals(ns) || COMMENT_NAMESPACE.equals(ns)) {
819                nas.id = localNameToId(((QNameResource) node).getLocalName());
820            } else {
821                nas.uri = ((Resource) node).getUri();
822            }
823        } else { // node.isResource()
824            String uri = ((Resource) node).getUri();
825            for (String ns : DOC_NAMESPACES) {
826                if (uri.startsWith(ns)) {
827                    nas.id = localNameToId(uri.substring(ns.length()));
828                    break;
829                }
830            }
831            if (nas.id == null) {
832                nas.uri = uri;
833            }
834        }
835        return nas;
836    }
837
838    protected static String localNameToId(String localName) {
839        String[] split = localName.split("/");
840        if (split.length == 1) {
841            return localName; // compat, no repository name
842        } else {
843            return split[1];
844        }
845    }
846
847    protected static String getAuthor(Statement statement) {
848        return getStringProperty(statement, RelationConstants.AUTHOR);
849    }
850
851    protected static void setAuthor(Statement statement, String author) {
852        setStringProperty(statement, RelationConstants.AUTHOR, author);
853    }
854
855    protected static Date getCreationDate(Statement statement) {
856        return getDateProperty(statement, RelationConstants.CREATION_DATE);
857    }
858
859    protected static void setCreationDate(Statement statement, Calendar created) {
860        setDateProperty(statement, RelationConstants.CREATION_DATE, created);
861    }
862
863    protected static Date getModificationDate(Statement statement) {
864        return getDateProperty(statement, RelationConstants.MODIFICATION_DATE);
865    }
866
867    protected static void setModificationDate(Statement statement, Calendar modified) {
868        setDateProperty(statement, RelationConstants.MODIFICATION_DATE, modified);
869    }
870
871    protected static String getComment(Statement statement) {
872        return getStringProperty(statement, RelationConstants.COMMENT);
873    }
874
875    protected static void setComment(Statement statement, String comment) {
876        setStringProperty(statement, RelationConstants.COMMENT, comment);
877    }
878
879    protected static String getStringProperty(Statement statement, Resource prop) {
880        Node node = statement.getProperty(prop);
881        if (node == null || !node.isLiteral()) {
882            return null;
883        }
884        return ((Literal) node).getValue();
885    }
886
887    protected static void setStringProperty(Statement statement, Resource prop, String string) {
888        if (string == null) {
889            return;
890        }
891        statement.setProperty(prop, NodeFactory.createLiteral(string));
892    }
893
894    protected static Date getDateProperty(Statement statement, Resource prop) {
895        Node node = statement.getProperty(prop);
896        if (node == null || !node.isLiteral()) {
897            return null;
898        }
899        return RelationDate.getDate((Literal) node);
900    }
901
902    protected static void setDateProperty(Statement statement, Resource prop, Calendar date) {
903        if (date == null) {
904            return;
905        }
906        statement.setProperty(prop, RelationDate.getLiteralDate(date));
907    }
908
909}