001/*
002 * ModeShape (http://www.modeshape.org)
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 */
016package org.modeshape.jdbc.delegate;
017
018import java.sql.DriverPropertyInfo;
019import java.sql.SQLException;
020import java.util.Collection;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Properties;
025import java.util.concurrent.atomic.AtomicReference;
026import javax.jcr.RepositoryException;
027import javax.jcr.nodetype.NodeType;
028import javax.jcr.query.QueryResult;
029import org.modeshape.jdbc.JcrDriver;
030import org.modeshape.jdbc.JdbcLocalI18n;
031import org.modeshape.jdbc.LocalJcrDriver.JcrContextFactory;
032import org.modeshape.jdbc.rest.ModeShapeRestClient;
033import org.modeshape.jdbc.rest.NodeTypes;
034import org.modeshape.jdbc.rest.Repositories;
035
036/**
037 * The HTTPRepositoryDelegate provides remote Repository implementation to access the Jcr layer via HTTP lookup.
038 */
039public class HttpRepositoryDelegate extends AbstractRepositoryDelegate {
040
041    protected static final int PROTOCOL_HTTP = 2;
042
043    public static final RepositoryDelegateFactory FACTORY = new RepositoryDelegateFactory() {
044
045        @Override
046        protected int determineProtocol( String url ) {
047            if (url.startsWith(JcrDriver.HTTP_URL_PREFIX) && url.length() > JcrDriver.HTTP_URL_PREFIX.length()) {
048                // This fits the pattern so far ...
049                return PROTOCOL_HTTP;
050            }
051            return super.determineProtocol(url);
052        }
053
054        @Override
055        protected RepositoryDelegate create( int protocol,
056                                             String url,
057                                             Properties info,
058                                             JcrContextFactory contextFactory ) {
059            if (protocol == PROTOCOL_HTTP) {
060                return new HttpRepositoryDelegate(url, info);
061            }
062            return super.create(protocol, url, info, contextFactory);
063        }
064    };
065
066    private static final String HTTP_EXAMPLE_URL = JcrDriver.HTTP_URL_PREFIX + "{hostname}:{port}/{context root}";
067    private AtomicReference<Map<String, NodeType>> nodeTypes = new AtomicReference<>();
068    private AtomicReference<Repositories.Repository> repository = new AtomicReference<>();
069    private ModeShapeRestClient restClient;
070
071    protected HttpRepositoryDelegate( String url,
072                                      Properties info ) {
073        super(url, info);
074    }
075
076    @Override
077    protected ConnectionInfo createConnectionInfo( String url,
078                                                   Properties info ) {
079        return new HttpConnectionInfo(url, info);
080    }
081
082    protected Repositories.Repository repository() {
083        return this.repository.get();
084    }
085
086    @Override
087    public QueryResult execute( String query,
088                                String language ) throws RepositoryException {
089        logger.trace("Executing query: {0}", query);
090        try {
091            org.modeshape.jdbc.rest.QueryResult result = this.restClient.query(query, language);
092            return new HttpQueryResult(result);
093        } catch (Exception e) {
094            throw new RepositoryException(e.getMessage(), e);
095        }
096    }
097
098    @Override
099    public String explain( String query,
100                           String language ) throws RepositoryException {
101        logger.trace("Explaining query: {0}", query);
102        try {
103            return this.restClient.queryPlan(query, language);
104        } catch (Exception e) {
105            throw new RepositoryException(e.getMessage(), e);
106        }
107    }
108
109    @Override
110    public String getDescriptor( String descriptorKey ) {
111        return repository() != null ? repository().getMetadata().get(descriptorKey).toString() : "";
112    }
113
114    @Override
115    public NodeType nodeType( String name ) throws RepositoryException {
116        if (nodeTypes.get() == null) {
117            // load the node types
118            nodeTypes();
119        }
120
121        NodeType nodetype = nodeTypes.get().get(name);
122        if (nodetype == null) {
123            throw new RepositoryException(JdbcLocalI18n.unableToGetNodeType.text(name));
124        }
125
126        return nodetype;
127    }
128
129    @Override
130    public Collection<NodeType> nodeTypes() throws RepositoryException {
131        Map<String, NodeType> nodeTypes = this.nodeTypes.get();
132        if (nodeTypes == null) {
133            NodeTypes restNodeTypes = this.restClient.getNodeTypes();
134            if (restNodeTypes.isEmpty()) {
135                throw new RepositoryException(JdbcLocalI18n.noNodeTypesReturned.text(restClient.serverUrl()));
136            }
137            nodeTypes = new HashMap<>();
138            for (org.modeshape.jdbc.rest.NodeType nodeType : restNodeTypes) {
139                nodeTypes.put(nodeType.getName(), nodeType);
140            }
141            this.nodeTypes.compareAndSet(null, nodeTypes);
142        }
143        return this.nodeTypes.get().values();
144    }
145
146    @Override
147    protected void initRepository() throws SQLException {
148        if (repository() != null) {
149            return;
150        }
151        logger.debug("Creating repository for HttpRepositoryDelegate");
152
153        ConnectionInfo info = getConnectionInfo();
154        assert info != null;
155
156        String path = info.getRepositoryPath();
157        if (path == null) {
158            throw new SQLException("Missing repo path from " + info.getUrl());
159        }
160        String username = info.getUsername();
161        if (username == null) {
162            throw new SQLException("Missing username from " + info.getUrl());
163        }
164        char[] password = info.getPassword();
165        if (password == null) {
166            throw new SQLException("Missing password path from " + info.getUrl());
167        }
168
169        String repositoryName = info.getRepositoryName();
170        if (repositoryName == null) {
171            throw new SQLException("Missing repository name from " + info.getUrl());
172        }
173
174        String serverUrl = "http://" + path + "/" + repositoryName;
175
176        String workspaceName = info.getWorkspaceName();
177        if (workspaceName == null) {
178            // there is no WS info, so try to figure out a default one...
179            ModeShapeRestClient client = new ModeShapeRestClient(serverUrl, username, String.valueOf(password));
180            List<String> allWorkspaces = client.getWorkspaces(repositoryName).getWorkspaces();
181            if (allWorkspaces.isEmpty()) {
182                throw new SQLException("No workspaces found for the " + repositoryName + " repository");
183            }
184            // TODO author=Horia Chiorean date=19-Aug-14 description=There is no way to get the "default" ws so we'll choose one
185            workspaceName = allWorkspaces.get(0);
186        }
187
188        serverUrl = serverUrl + "/" + workspaceName;
189        logger.debug("Using server url: {0}", serverUrl);
190        // this is only a connection test to confirm a connection can be made and results can be obtained.
191        try {
192            this.restClient = new ModeShapeRestClient(serverUrl, username, String.valueOf(password));
193            Repositories repositories = this.restClient.getRepositories();
194            this.setRepositoryNames(repositories.getRepositoryNames());
195            Repositories.Repository repository = repositories.getRepository(repositoryName);
196            if (repository == null) {
197                throw new SQLException(JdbcLocalI18n.unableToFindNamedRepository.text(path, repositoryName));
198            }
199            this.repository.compareAndSet(null, repository);
200        } catch (Exception e) {
201            throw new SQLException(JdbcLocalI18n.noRepositoryNamesFound.text(), e);
202        }
203    }
204
205    @Override
206    public boolean isValid( final int timeout ) {
207        try {
208            this.restClient.getWorkspaces(getConnectionInfo().getRepositoryName());
209            return true;
210        } catch (Throwable e) {
211            return false;
212        }
213    }
214
215    @Override
216    public void close() {
217        super.close();
218        restClient = null;
219        nodeTypes.set(null);
220        repository.set(null);
221    }
222
223    private class HttpConnectionInfo extends ConnectionInfo {
224
225        protected HttpConnectionInfo( String url,
226                                      Properties properties ) {
227            super(url, properties);
228        }
229
230        @Override
231        protected void init() {
232            // parsing 2 ways of specifying the repository and workspace
233            // 1) defined using ?repositoryName
234            // 2) defined in the path server:8080/modeshape-rest/respositoryName/workspaceName
235
236            super.init();
237
238            // if the workspace and/or repository name is not specified as a property on the url,
239            // then parse the url to obtain the values from the path, the url must be in the format:
240            // {hostname}:{port} / {context root} + / respositoryName / workspaceName
241
242            StringBuilder url = new StringBuilder();
243            String[] urlsections = repositoryPath.split("/");
244            // if there are only 2 sections, then the url can have the workspace or repository name specified in the path
245            if (urlsections.length < 3) {
246                return;
247            }
248
249            // the assignment of url section is working back to front, this is so in cases where
250            // the {context} is changed to be made up of multiple sections, instead of the default (modeshape-rest), the
251            // workspace should be the last section (if exist) and the repository should be before the
252            // workspace.
253            int workspacePos = -1;
254            int repositoryPos = -1;
255            int repoPos = 1;
256            if (this.getWorkspaceName() == null && urlsections.length > 3) {
257                workspacePos = urlsections.length - 1;
258                String workspaceName = urlsections[workspacePos];
259                this.setWorkspaceName(workspaceName);
260                // if workspace is found, then repository is assume in the prior section
261                repoPos = 2;
262
263            }
264            if (this.getRepositoryName() == null && urlsections.length > 2) {
265                repositoryPos = urlsections.length - repoPos;
266                String repositoryName = urlsections[repositoryPos];
267                this.setRepositoryName(repositoryName);
268            }
269
270            // rebuild the url without the repositoryName or WorkspaceName because
271            // the createConnection() needs these separated.
272            for (int i = 0; i < repositoryPos; i++) {
273                url.append(urlsections[i]);
274                if (i < repositoryPos - 1) {
275                    url.append("/");
276                }
277            }
278
279            this.repositoryPath = url.toString();
280        }
281
282        @Override
283        public String getUrlExample() {
284            return HTTP_EXAMPLE_URL;
285        }
286
287        @Override
288        public String getUrlPrefix() {
289            return JcrDriver.HTTP_URL_PREFIX;
290        }
291
292        @Override
293        protected void addUrlPropertyInfo( List<DriverPropertyInfo> results ) {
294            // if the repository path doesn't have at least the {context}
295            // example: server:8080/modeshape-rest where modeshape-rest is the context,
296            // then the URL is considered invalid.
297            if (!repositoryPath.contains("/")) {
298                setUrl(null);
299            }
300            super.addUrlPropertyInfo(results);
301        }
302    }
303}