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