001/*
002 * (C) Copyright 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 */
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.lang.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 String BLANK_NS = "-:";
112
113    public static String DOCUMENT_NAMESPACE = RelationConstants.DOCUMENT_NAMESPACE;
114
115    /** Without final slash (compat). */
116    public static 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.<String> emptyList() : new ArrayList<String>(
159                new LinkedHashSet<String>(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.getLocalService(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            IterableQueryResult it = session.queryAndFetch("SELECT " + NXQL.ECM_UUID + " FROM " + docType, NXQL.NXQL);
210            try {
211                size = it.size();
212            } finally {
213                it.close();
214            }
215        }
216    }
217
218    @Override
219    public void clear() {
220        remove(Collections.singletonList(ALL));
221    }
222
223    @Override
224    public void add(Statement statement) {
225        add(Collections.singletonList(statement));
226    }
227
228    @Override
229    public void add(List<Statement> statements) {
230        StatementAdder statementAdder = session == null ? new StatementAdder(statements) : new StatementAdder(
231                statements, session);
232        statementAdder.runUnrestricted();
233    }
234
235    protected class StatementAdder extends UnrestrictedSessionRunner {
236
237        protected List<Statement> statements;
238
239        protected Date now;
240
241        protected StatementAdder(List<Statement> statements) {
242            super(getDefaultRepositoryName(), "system");
243            this.statements = statements;
244        }
245
246        protected StatementAdder(List<Statement> statements, CoreSession session) {
247            super(session);
248            this.statements = statements;
249        }
250
251        @Override
252        public void run() {
253            now = new Date();
254            for (Statement statement : statements) {
255                add(statement);
256            }
257            session.save();
258        }
259
260        protected void add(Statement statement) {
261            DocumentModel rel = session.createDocumentModel(null, "relation", docType);
262            rel = setRelationProperties(rel, statement);
263            session.createDocument(rel);
264        }
265
266        protected DocumentModel setRelationProperties(DocumentModel rel, Statement statement) {
267            Resource pred = statement.getPredicate();
268            String predicateUri = pred.getUri();
269            if (predicateUri == null) {
270                throw new IllegalArgumentException("Invalid predicate in statement: " + statement);
271            }
272
273            Subject subject = statement.getSubject();
274            if (subject.isLiteral()) {
275                throw new IllegalArgumentException("Invalid literal subject in statement: " + statement);
276            }
277            NodeAsString source = getNodeAsString(subject);
278
279            Node object = statement.getObject();
280            NodeAsString target = getNodeAsString(object);
281
282            String author = getAuthor(statement);
283            if (author == null) {
284                author = getOriginatingUsername();
285            }
286
287            Date created = getCreationDate(statement);
288            if (created == null) {
289                created = now;
290            }
291
292            Date modified = getModificationDate(statement);
293            if (modified == null) {
294                modified = now;
295            }
296
297            String comment = getComment(statement);
298
299            String title = (source.id != null ? source.id : source.uri) + " "
300                    + predicateUri.substring(predicateUri.lastIndexOf('/') + 1) + " "
301                    + (target.id != null ? target.id : target.uri != null ? target.uri : target.string);
302            int MAX_TITLE = 200;
303            if (title.length() > MAX_TITLE) {
304                title = title.substring(0, MAX_TITLE);
305            }
306
307            rel.setPropertyValue(REL_PREDICATE, predicateUri);
308            if (source.id != null) {
309                rel.setPropertyValue(REL_SOURCE_ID, source.id);
310            } else {
311                rel.setPropertyValue(REL_SOURCE_URI, source.uri);
312            }
313            if (target.id != null) {
314                rel.setPropertyValue(REL_TARGET_ID, target.id);
315            } else if (target.uri != null) {
316                rel.setPropertyValue(REL_TARGET_URI, target.uri);
317            } else {
318                rel.setPropertyValue(REL_TARGET_STRING, target.string);
319            }
320            if (author != null) {
321                // will usually get overwritten by DublinCoreListener
322                // but not in tests
323                rel.setPropertyValue(DC_CREATOR, author);
324            }
325            if (created != null) {
326                // will usually get overwritten by DublinCoreListener
327                // but not in tests
328                rel.setPropertyValue(DC_CREATED, created);
329            }
330            if (modified != null) {
331                // will usually get overwritten by DublinCoreListener
332                // but not in tests
333                rel.setPropertyValue(DC_MODIFIED, modified);
334            }
335            rel.setPropertyValue(DC_TITLE, title); // for debug
336            if (comment != null) {
337                rel.setPropertyValue(DC_DESCRIPTION, comment);
338            }
339            return rel;
340        }
341    }
342
343    @Override
344    public void remove(Statement statement) {
345        remove(Collections.singletonList(statement));
346    }
347
348    @Override
349    public void remove(List<Statement> statements) {
350        StatementRemover statementRemover = session == null ? new StatementRemover(statements) : new StatementRemover(
351                statements, session);
352        statementRemover.runUnrestricted();
353    }
354
355    protected class StatementRemover extends UnrestrictedSessionRunner {
356
357        protected List<Statement> statements;
358
359        protected Date now;
360
361        protected StatementRemover(List<Statement> statements) {
362            super(getDefaultRepositoryName());
363            this.statements = statements;
364        }
365
366        protected StatementRemover(List<Statement> statements, CoreSession session) {
367            super(session);
368            this.statements = statements;
369        }
370
371        @Override
372        public void run() {
373            now = new Date();
374            for (Statement statement : statements) {
375                remove(statement);
376            }
377        }
378
379        protected void remove(Statement statement) {
380            String query = "SELECT " + NXQL.ECM_UUID + " FROM " + docType;
381            query = whereBuilder(query, statement);
382            if (query == null) {
383                return;
384            }
385            IterableQueryResult it = session.queryAndFetch(query, NXQL.NXQL);
386            try {
387                for (Map<String, Serializable> map : it) {
388                    String id = (String) map.get(NXQL.ECM_UUID);
389                    session.removeDocument(new IdRef(id));
390                }
391            } finally {
392                it.close();
393            }
394        }
395    }
396
397    protected class StatementFinder extends UnrestrictedSessionRunner {
398
399        protected List<Statement> statements;
400
401        protected Statement statement;
402
403        protected StatementFinder(Statement statement) {
404            super(getDefaultRepositoryName());
405            this.statement = statement;
406        }
407
408        protected StatementFinder(Statement statement, CoreSession session) {
409            super(session);
410            this.statement = statement;
411        }
412
413        @Override
414        public void run() {
415            String query = "SELECT " + REL_PREDICATE + ", " + REL_SOURCE_ID + ", " + REL_SOURCE_URI + ", "
416                    + REL_TARGET_ID + ", " + REL_TARGET_URI + ", " + REL_TARGET_STRING + ", " + DC_CREATED + ", "
417                    + DC_CREATOR + ", " + DC_MODIFIED + ", " + DC_DESCRIPTION + " FROM " + docType;
418            query = whereBuilder(query, statement);
419            if (query == null) {
420                statements = EMPTY_STATEMENTS;
421                return;
422            }
423            statements = new ArrayList<Statement>();
424            IterableQueryResult it = session.queryAndFetch(query, NXQL.NXQL);
425            try {
426                for (Map<String, Serializable> map : it) {
427                    String pred = (String) map.get(REL_PREDICATE);
428                    String source = (String) map.get(REL_SOURCE_ID);
429                    String sourceUri = (String) map.get(REL_SOURCE_URI);
430                    String target = (String) map.get(REL_TARGET_ID);
431                    String targetUri = (String) map.get(REL_TARGET_URI);
432                    String targetString = (String) map.get(REL_TARGET_STRING);
433                    Calendar created = (Calendar) map.get(DC_CREATED);
434                    String creator = (String) map.get(DC_CREATOR);
435                    Calendar modified = (Calendar) map.get(DC_MODIFIED);
436                    String comment = (String) map.get(DC_DESCRIPTION);
437
438                    Resource predicate = NodeFactory.createResource(pred);
439                    Node subject;
440                    if (source != null) {
441                        subject = createId(source);
442                    } else {
443                        subject = createUri(sourceUri);
444                    }
445                    Node object;
446                    if (target != null) {
447                        object = createId(target);
448                    } else if (targetUri != null) {
449                        object = createUri(targetUri);
450                    } else {
451                        object = NodeFactory.createLiteral(targetString);
452                    }
453                    Statement statement = new StatementImpl(subject, predicate, object);
454                    setCreationDate(statement, created);
455                    setAuthor(statement, creator);
456                    setModificationDate(statement, modified);
457                    setComment(statement, comment);
458                    statements.add(statement);
459                }
460            } finally {
461                it.close();
462            }
463        }
464
465        protected QNameResource createId(String id) {
466            return NodeFactory.createQNameResource(DOCUMENT_NAMESPACE, session.getRepositoryName() + '/' + id);
467        }
468
469        protected Node createUri(String uri) {
470            if (uri.startsWith(BLANK_NS)) {
471                // skolemization
472                String id = uri.substring(BLANK_NS.length());
473                return NodeFactory.createBlank(id.isEmpty() ? null : id);
474            } else {
475                for (String ns : namespaceList) {
476                    if (uri.startsWith(ns)) {
477                        String localName = uri.substring(ns.length());
478                        return NodeFactory.createQNameResource(ns, localName);
479                    }
480                }
481                return NodeFactory.createResource(uri);
482            }
483        }
484
485    }
486
487    @Override
488    public List<Statement> getStatements() {
489        return getStatements(ALL);
490    }
491
492    @Override
493    public List<Statement> getStatements(Node subject, Node predicate, Node object) {
494        return getStatements(new StatementImpl(subject, predicate, object));
495    }
496
497    @Override
498    public List<Statement> getStatements(Statement statement) {
499        StatementFinder statementFinder = session == null ? new StatementFinder(statement) : new StatementFinder(
500                statement, session);
501        statementFinder.runUnrestricted();
502        return statementFinder.statements;
503    }
504
505    @Override
506    public List<Node> getSubjects(Node predicate, Node object) {
507        List<Statement> statements = getStatements(new StatementImpl(null, predicate, object));
508        List<Node> nodes = new ArrayList<Node>(statements.size());
509        for (Statement statement : statements) {
510            nodes.add(statement.getSubject());
511        }
512        return nodes;
513    }
514
515    @Override
516    public List<Node> getPredicates(Node subject, Node object) {
517        List<Statement> statements = getStatements(new StatementImpl(subject, null, object));
518        List<Node> nodes = new ArrayList<Node>(statements.size());
519        for (Statement statement : statements) {
520            nodes.add(statement.getPredicate());
521        }
522        return nodes;
523    }
524
525    @Override
526    public List<Node> getObjects(Node subject, Node predicate) {
527        List<Statement> statements = getStatements(new StatementImpl(subject, predicate, null));
528        List<Node> nodes = new ArrayList<Node>(statements.size());
529        for (Statement statement : statements) {
530            nodes.add(statement.getObject());
531        }
532        return nodes;
533    }
534
535    @Override
536    public boolean hasStatement(Statement statement) {
537        if (statement == null) {
538            return false;
539        }
540        // could be optimized in the null/blank case, but this method seems
541        // unused
542        return !getStatements(statement).isEmpty();
543    }
544
545    @Override
546    public boolean hasResource(Resource resource) {
547        if (resource == null) {
548            return false;
549        }
550        ResourceFinder resourceFinder = session == null ? new ResourceFinder(resource) : new ResourceFinder(resource,
551                session);
552        resourceFinder.runUnrestricted();
553        return resourceFinder.found;
554    }
555
556    protected class ResourceFinder extends UnrestrictedSessionRunner {
557
558        protected boolean found;
559
560        protected Resource resource;
561
562        protected ResourceFinder(Resource resource) {
563            super(getDefaultRepositoryName());
564            this.resource = resource;
565        }
566
567        protected ResourceFinder(Resource resource, CoreSession session) {
568            super(session);
569            this.resource = resource;
570        }
571
572        @Override
573        public void run() {
574            String query = "SELECT " + NXQL.ECM_UUID + " FROM " + docType;
575            query = whereAnyBuilder(query, resource);
576            IterableQueryResult it = session.queryAndFetch(query, NXQL.NXQL);
577            try {
578                found = it.iterator().hasNext();
579            } finally {
580                it.close();
581            }
582        }
583
584        protected String whereAnyBuilder(String query, Resource resource) {
585            List<Object> params = new ArrayList<Object>(3);
586            List<String> clauses = new ArrayList<String>(3);
587
588            NodeAsString nas = getNodeAsString(resource);
589            if (nas.id != null) {
590                // don't allow predicates that are actually doc ids
591                clauses.add(REL_SOURCE_ID + " = ?");
592                params.add(nas.id);
593                clauses.add(REL_TARGET_URI + " = ?");
594                params.add(DOCUMENT_NAMESPACE + session.getRepositoryName() + '/' + nas.id);
595            } else if (nas.uri != null) {
596                for (String ns : DOC_NAMESPACES) {
597                    if (nas.uri.startsWith(ns)) {
598                        String id = localNameToId(nas.uri.substring(ns.length()));
599                        clauses.add(REL_SOURCE_ID + " = ?");
600                        params.add(id);
601                        break;
602                    }
603                }
604                clauses.add(REL_SOURCE_URI + " = ?");
605                params.add(nas.uri);
606                clauses.add(REL_TARGET_URI + " = ?");
607                params.add(nas.uri);
608                clauses.add(REL_PREDICATE + " = ?");
609                params.add(nas.uri);
610            }
611            query += " WHERE " + StringUtils.join(clauses, " OR ");
612            query = NXQLQueryBuilder.getQuery(query, params.toArray(), true, true, null);
613            return query;
614        }
615    }
616
617    public static final Pattern SPARQL_SPO_PO = Pattern.compile("SELECT \\?s \\?p \\?o WHERE \\{ \\?s \\?p \\?o . \\?s <(.*)> <(.*)> . \\}");
618
619    public static final Pattern SPARQL_PO_S = Pattern.compile("SELECT \\?p \\?o WHERE \\{ <(.*)> \\?p \\?o \\}");
620
621    @Override
622    public QueryResult query(String queryString, String language, String baseURI) {
623        // language is ignored, assume SPARQL
624        Matcher matcher = SPARQL_SPO_PO.matcher(queryString);
625        if (matcher.matches()) {
626            Node predicate = NodeFactory.createResource(matcher.group(1));
627            Node object = NodeFactory.createResource(matcher.group(2));
628            // find subjects with this predicate and object
629            List<Node> subjects = getSubjects(predicate, object);
630            List<Map<String, Node>> results = new ArrayList<>();
631            if (!subjects.isEmpty()) {
632                // find all statements with these subjects
633                List<Statement> statements = getStatements(new Subjects(subjects), null, null);
634                for (Statement st : statements) {
635                    Map<String, Node> map = new HashMap<>();
636                    map.put("s", st.getSubject());
637                    map.put("p", st.getPredicate());
638                    map.put("o", st.getObject());
639                    results.add(map);
640                }
641            }
642            return new QueryResultImpl(Integer.valueOf(results.size()), Arrays.asList("s", "p", "o"), results);
643        }
644        matcher = SPARQL_PO_S.matcher(queryString);
645        if (matcher.matches()) {
646            Node subject = NodeFactory.createResource(matcher.group(1));
647            // find predicates and objects with this subject
648            List<Statement> statements = getStatements(new StatementImpl(subject, null, null));
649            List<Map<String, Node>> results = new ArrayList<>();
650            for (Statement st : statements) {
651                Map<String, Node> map = new HashMap<>();
652                map.put("p", st.getPredicate());
653                map.put("o", st.getObject());
654                results.add(map);
655            }
656            return new QueryResultImpl(Integer.valueOf(results.size()), Arrays.asList("p", "o"), results);
657        }
658        throw new UnsupportedOperationException("Cannot parse query: " + queryString);
659    }
660
661    public static final Pattern SPARQL_S_PO = Pattern.compile("SELECT \\?s WHERE \\{ \\?s <(.*)> <(.*)> \\}");
662
663    @Override
664    public int queryCount(String queryString, String language, String baseURI) {
665        // language is ignored, assume SPARQL
666        Matcher matcher = SPARQL_S_PO.matcher(queryString);
667        if (matcher.matches()) {
668            Node predicate = NodeFactory.createResource(matcher.group(1));
669            Node object = NodeFactory.createResource(matcher.group(2));
670            List<Node> subjects = getSubjects(predicate, object);
671            return subjects.size();
672        }
673        throw new UnsupportedOperationException("Cannot parse query: " + queryString);
674    }
675
676    @Override
677    public boolean read(String path, String lang, String base) {
678        InputStream in = null;
679        try {
680            in = new FileInputStream(path);
681            return read(in, lang, base);
682        } catch (FileNotFoundException e) {
683            throw new RuntimeException(e);
684        } finally {
685            if (in != null) {
686                try {
687                    in.close();
688                } catch (IOException e) {
689                    log.error(e);
690                }
691            }
692        }
693    }
694
695    @Override
696    public boolean write(String path, String lang, String base) {
697        OutputStream out = null;
698        try {
699            out = new FileOutputStream(new File(path));
700            return write(out, lang, base);
701        } catch (FileNotFoundException e) {
702            throw new RuntimeException(e);
703        } finally {
704            if (out != null) {
705                try {
706                    out.close();
707                } catch (IOException e) {
708                    log.error(e);
709                }
710            }
711        }
712    }
713
714    @Override
715    public boolean read(InputStream in, String lang, String base) {
716        throw new UnsupportedOperationException();
717    }
718
719    @Override
720    public boolean write(OutputStream out, String lang, String base) {
721        throw new UnsupportedOperationException();
722    }
723
724    protected static String getDefaultRepositoryName() {
725        return Framework.getService(RepositoryManager.class).getDefaultRepositoryName();
726    }
727
728    /** Fake Node type used to pass down multiple nodes into whereBuilder. */
729    public static class Subjects extends AbstractNode implements Subject {
730
731        private static final long serialVersionUID = 1L;
732
733        protected List<Node> nodes;
734
735        public Subjects(List<Node> nodes) {
736            this.nodes = nodes;
737        }
738
739        public List<Node> getNodes() {
740            return nodes;
741        }
742
743        @Override
744        public NodeType getNodeType() {
745            return null;
746        }
747    }
748
749    protected String whereBuilder(String query, Statement statement) {
750        List<Object> params = new ArrayList<Object>(3);
751        List<String> clauses = new ArrayList<String>(3);
752
753        Resource p = statement.getPredicate();
754        if (p != null) {
755            NodeAsString pn = getNodeAsString(p);
756            if (pn.uri == null) {
757                return null;
758            }
759            clauses.add(REL_PREDICATE + " = ?");
760            params.add(pn.uri);
761        }
762
763        Node s = statement.getSubject();
764        if (s != null) {
765            if (s instanceof Subjects) {
766                List<Node> subjects = ((Subjects) s).getNodes();
767                if (subjects.isEmpty()) {
768                    throw new UnsupportedOperationException("empty subjects");
769                }
770                StringBuilder buf = new StringBuilder(REL_SOURCE_URI);
771                buf.append(" IN (");
772                for (Node sub : subjects) {
773                    NodeAsString sn = getNodeAsString(sub);
774                    if (sn.id != null) {
775                        throw new UnsupportedOperationException("subjects ListNode with id instead of uri" + subjects);
776                    }
777                    buf.append("?, ");
778                    params.add(sn.uri);
779                }
780                buf.setLength(buf.length() - 2); // remove last comma/space
781                buf.append(")");
782                clauses.add(buf.toString());
783            } else {
784                NodeAsString sn = getNodeAsString(s);
785                if (sn.id != null) {
786                    clauses.add(REL_SOURCE_ID + " = ?");
787                    params.add(sn.id);
788                } else {
789                    clauses.add(REL_SOURCE_URI + " = ?");
790                    params.add(sn.uri);
791                }
792
793            }
794        }
795
796        Node o = statement.getObject();
797        if (o != null) {
798            NodeAsString on = getNodeAsString(o);
799            if (on.id != null) {
800                clauses.add(REL_TARGET_ID + " = ?");
801                params.add(on.id);
802            } else if (on.uri != null) {
803                clauses.add(REL_TARGET_URI + " = ?");
804                params.add(on.uri);
805            } else {
806                clauses.add(REL_TARGET_STRING + " = ?");
807                params.add(on.string);
808            }
809        }
810
811        if (!clauses.isEmpty()) {
812            query += " WHERE " + StringUtils.join(clauses, " AND ");
813            query = NXQLQueryBuilder.getQuery(query, params.toArray(), true, true, null);
814        }
815        return query;
816    }
817
818    protected static NodeAsString getNodeAsString(Node node) {
819        NodeAsString nas = new NodeAsString();
820        if (node.isBlank()) {
821            // skolemization
822            String id = ((Blank) node).getId();
823            nas.uri = BLANK_NS + (id == null ? "" : id);
824        } else if (node.isLiteral()) {
825            nas.string = ((Literal) node).getValue();
826        } else if (node.isQNameResource()) {
827            String ns = ((QNameResource) node).getNamespace();
828            if (DOCUMENT_NAMESPACE.equals(ns) || DOCUMENT_NAMESPACE2.equals(ns) || COMMENT_NAMESPACE.equals(ns)) {
829                nas.id = localNameToId(((QNameResource) node).getLocalName());
830            } else {
831                nas.uri = ((Resource) node).getUri();
832            }
833        } else { // node.isResource()
834            String uri = ((Resource) node).getUri();
835            for (String ns : DOC_NAMESPACES) {
836                if (uri.startsWith(ns)) {
837                    nas.id = localNameToId(uri.substring(ns.length()));
838                    break;
839                }
840            }
841            if (nas.id == null) {
842                nas.uri = uri;
843            }
844        }
845        return nas;
846    }
847
848    protected static String localNameToId(String localName) {
849        String[] split = localName.split("/");
850        if (split.length == 1) {
851            return localName; // compat, no repository name
852        } else {
853            return split[1];
854        }
855    }
856
857    protected static String getAuthor(Statement statement) {
858        return getStringProperty(statement, RelationConstants.AUTHOR);
859    }
860
861    protected static void setAuthor(Statement statement, String author) {
862        setStringProperty(statement, RelationConstants.AUTHOR, author);
863    }
864
865    protected static Date getCreationDate(Statement statement) {
866        return getDateProperty(statement, RelationConstants.CREATION_DATE);
867    }
868
869    protected static void setCreationDate(Statement statement, Calendar created) {
870        setDateProperty(statement, RelationConstants.CREATION_DATE, created);
871    }
872
873    protected static Date getModificationDate(Statement statement) {
874        return getDateProperty(statement, RelationConstants.MODIFICATION_DATE);
875    }
876
877    protected static void setModificationDate(Statement statement, Calendar modified) {
878        setDateProperty(statement, RelationConstants.MODIFICATION_DATE, modified);
879    }
880
881    protected static String getComment(Statement statement) {
882        return getStringProperty(statement, RelationConstants.COMMENT);
883    }
884
885    protected static void setComment(Statement statement, String comment) {
886        setStringProperty(statement, RelationConstants.COMMENT, comment);
887    }
888
889    protected static String getStringProperty(Statement statement, Resource prop) {
890        Node node = statement.getProperty(prop);
891        if (node == null || !node.isLiteral()) {
892            return null;
893        }
894        return ((Literal) node).getValue();
895    }
896
897    protected static void setStringProperty(Statement statement, Resource prop, String string) {
898        if (string == null) {
899            return;
900        }
901        statement.setProperty(prop, NodeFactory.createLiteral(string));
902    }
903
904    protected static Date getDateProperty(Statement statement, Resource prop) {
905        Node node = statement.getProperty(prop);
906        if (node == null || !node.isLiteral()) {
907            return null;
908        }
909        return RelationDate.getDate((Literal) node);
910    }
911
912    protected static void setDateProperty(Statement statement, Resource prop, Calendar date) {
913        if (date == null) {
914            return;
915        }
916        statement.setProperty(prop, RelationDate.getLiteralDate(date));
917    }
918
919}