001    /*
002     * JBoss DNA (http://www.jboss.org/dna)
003     * See the COPYRIGHT.txt file distributed with this work for information
004     * regarding copyright ownership.  Some portions may be licensed
005     * to Red Hat, Inc. under one or more contributor license agreements.
006     * See the AUTHORS.txt file in the distribution for a full listing of 
007     * individual contributors. 
008     *
009     * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
010     * is licensed to you under the terms of the GNU Lesser General Public License as
011     * published by the Free Software Foundation; either version 2.1 of
012     * the License, or (at your option) any later version.
013     *
014     * JBoss DNA is distributed in the hope that it will be useful,
015     * but WITHOUT ANY WARRANTY; without even the implied warranty of
016     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017     * Lesser General Public License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this software; if not, write to the Free
021     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
022     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
023     */
024    package org.jboss.example.dna.repository;
025    
026    import java.io.IOException;
027    import java.util.ArrayList;
028    import java.util.Collections;
029    import java.util.HashSet;
030    import java.util.List;
031    import java.util.Map;
032    import java.util.Set;
033    import java.util.concurrent.TimeUnit;
034    import javax.jcr.Credentials;
035    import javax.jcr.Node;
036    import javax.jcr.NodeIterator;
037    import javax.jcr.PropertyIterator;
038    import javax.jcr.Session;
039    import javax.jcr.Value;
040    import javax.security.auth.login.LoginContext;
041    import javax.security.auth.login.LoginException;
042    import net.jcip.annotations.Immutable;
043    import org.jboss.dna.common.text.NoOpEncoder;
044    import org.jboss.dna.common.util.CheckArg;
045    import org.jboss.dna.graph.ExecutionContext;
046    import org.jboss.dna.graph.Graph;
047    import org.jboss.dna.graph.JaasSecurityContext;
048    import org.jboss.dna.graph.Location;
049    import org.jboss.dna.graph.property.Path;
050    import org.jboss.dna.graph.property.PathFactory;
051    import org.jboss.dna.graph.property.PathNotFoundException;
052    import org.jboss.dna.graph.property.Property;
053    import org.jboss.dna.jcr.JcrConfiguration;
054    import org.jboss.dna.jcr.JcrEngine;
055    import org.jboss.dna.jcr.JcrRepository;
056    import org.jboss.security.config.IDTrustConfiguration;
057    import org.xml.sax.SAXException;
058    
059    /**
060     * @author Randall Hauch
061     */
062    public class RepositoryClient {
063    
064        public static final String INMEMORY_REPOSITORY_SOURCE_CLASSNAME = "org.jboss.dna.connector.inmemory.InMemoryRepositorySource";
065        public static final String JAAS_LOGIN_CONTEXT_NAME = "dna-jcr";
066    
067        /**
068         * @param args
069         */
070        public static void main( String[] args ) {
071            // Set up the JAAS provider (IDTrust) and a policy file (which defines the "dna-jcr" login config name)
072            IDTrustConfiguration idtrustConfig = new IDTrustConfiguration();
073            try {
074                idtrustConfig.config("security/jaas.conf.xml");
075            } catch (Exception ex) {
076                throw new IllegalStateException(ex);
077            }
078    
079            // Now configure the repository client component ...
080            RepositoryClient client = new RepositoryClient();
081            for (String arg : args) {
082                arg = arg.trim();
083                if (arg.equals("--api=jcr")) client.setApi(Api.JCR);
084                if (arg.equals("--api=dna")) client.setApi(Api.DNA);
085                if (arg.equals("--jaas")) client.setJaasContextName(JAAS_LOGIN_CONTEXT_NAME);
086                if (arg.startsWith("--jaas=") && arg.length() > 7) client.setJaasContextName(arg.substring(7).trim());
087            }
088    
089            // And have it use a ConsoleInput user interface ...
090            client.setUserInterface(new ConsoleInput(client, args));
091        }
092    
093        public enum Api {
094            JCR,
095            DNA;
096        }
097    
098        private Api api = Api.JCR;
099        private String jaasContextName = JAAS_LOGIN_CONTEXT_NAME;
100        private UserInterface userInterface;
101        private LoginContext loginContext;
102        private JcrEngine engine;
103    
104        /**
105         * @param userInterface Sets userInterface to the specified value.
106         */
107        public void setUserInterface( UserInterface userInterface ) {
108            this.userInterface = userInterface;
109        }
110    
111        /**
112         * Set the API that this client should use to interact with the repositories.
113         * 
114         * @param api The API that should be used
115         */
116        public void setApi( Api api ) {
117            this.api = api != null ? api : Api.DNA;
118        }
119    
120        /**
121         * Set the JAAS context name that should be used. If null (which is the default), then no authentication will be used.
122         * 
123         * @param jaasContextName the JAAS context name, or null if no authentication should be performed
124         */
125        public void setJaasContextName( String jaasContextName ) {
126            this.jaasContextName = jaasContextName;
127        }
128    
129        /**
130         * Start up the repositories. This method loads the configuration, then creates the engine and starts it.
131         * 
132         * @throws IOException if there is a problem initializing the repositories from the files.
133         * @throws SAXException if there is a problem with the SAX Parser
134         */
135        public void startRepositories() throws IOException, SAXException {
136            if (engine != null) return; // already started
137    
138            // Load the configuration from a file, as provided by the user interface ...
139            JcrConfiguration configuration = new JcrConfiguration();
140            configuration.loadFrom(userInterface.getRepositoryConfiguration());
141    
142            // Load the node types for each JCR repository, via a CND file. These could have been defined
143            // in the configuration file, but this approach is easy and allows us to define the node types
144            // using the CND format in one or multiple files.
145            String locationOfCndFiles = userInterface.getLocationOfCndFiles();
146            configuration.repository("Aircraft").addNodeTypes(locationOfCndFiles + "/aircraft.cnd");
147            configuration.repository("Cars").addNodeTypes(locationOfCndFiles + "/cars.cnd");
148            configuration.repository("Vehicles").addNodeTypes(locationOfCndFiles + "/vehicles.cnd");
149    
150            // Now create the JCR engine ...
151            engine = configuration.build();
152            engine.start();
153    
154            // For this example, we're using a couple of in-memory repositories (including one for the configuration repository).
155            // Normally, these would exist already and would simply be accessed. But in this example, we're going to
156            // populate these repositories here by importing from files. First do the configuration repository ...
157            String location = this.userInterface.getLocationOfRepositoryFiles();
158    
159            // Now import the content for the two in-memory repository sources ...
160            engine.getGraph("Cars").importXmlFrom(location + "/cars.xml").into("/");
161            engine.getGraph("Aircraft").importXmlFrom(location + "/aircraft.xml").into("/");
162        }
163    
164        /**
165         * Get the names of the repositories.
166         * 
167         * @return the sorted but immutable list of repository names; never null
168         */
169        public List<String> getNamesOfRepositories() {
170            List<String> names = new ArrayList<String>(engine.getRepositoryNames());
171            Collections.sort(names);
172            return Collections.unmodifiableList(names);
173        }
174    
175        /**
176         * Shut down the components and services and blocking until all resources have been released.
177         * 
178         * @throws InterruptedException if the thread was interrupted before completing the shutdown.
179         * @throws LoginException
180         */
181        public void shutdown() throws InterruptedException, LoginException {
182            logout();
183            if (engine == null) return;
184            try {
185                // Tell the engine to shut down, and then wait up to 5 seconds for it to complete...
186                engine.shutdown();
187                engine.awaitTermination(5, TimeUnit.SECONDS);
188            } finally {
189                engine = null;
190            }
191        }
192    
193        /**
194         * Get the current JAAS LoginContext (if there is one).
195         * 
196         * @return the current login context, or null if no JAAS authentication is to be used.
197         * @throws LoginException if authentication was attempted but failed
198         */
199        protected LoginContext getLoginContext() throws LoginException {
200            if (loginContext == null) {
201                if (jaasContextName != null) {
202                    loginContext = new LoginContext(jaasContextName, this.userInterface.getCallbackHandler());
203                    loginContext.login(); // This authenticates the user
204                }
205            }
206            return loginContext;
207        }
208    
209        /**
210         * Calling this will lose the context
211         * 
212         * @throws LoginException
213         */
214        public void logout() throws LoginException {
215            if (loginContext != null) {
216                try {
217                    loginContext.logout();
218                } finally {
219                    loginContext = null;
220                }
221            }
222        }
223    
224        /**
225         * Get the information about a node, using the {@link #setApi(Api) API} method.
226         * 
227         * @param sourceName the name of the repository source
228         * @param pathToNode the path to the node in the repository that is to be retrieved
229         * @param properties the map into which the property values will be placed; may be null if the properties are not to be
230         *        retrieved
231         * @param children the collection into which the child names should be placed; may be null if the children are not to be
232         *        retrieved
233         * @return true if the node was found, or false if it was not
234         * @throws Throwable
235         */
236        public boolean getNodeInfo( String sourceName,
237                                    String pathToNode,
238                                    Map<String, Object[]> properties,
239                                    List<String> children ) throws Throwable {
240            LoginContext loginContext = getLoginContext(); // will ask user to authenticate if needed
241            switch (api) {
242                case JCR: {
243                    JcrRepository jcrRepository = engine.getRepository(sourceName);
244                    Session session = null;
245                    if (loginContext != null) {
246                        // Could also use SimpleCredentials(username,password) too
247                        Credentials credentials = new JaasCredentials(loginContext);
248                        session = jcrRepository.login(credentials);
249                    } else {
250                        session = jcrRepository.login();
251                    }
252                    try {
253                        // Make the path relative to the root by removing the leading slash(es) ...
254                        pathToNode = pathToNode.replaceAll("^/+", "");
255                        // Get the node by path ...
256                        Node root = session.getRootNode();
257                        Node node = root;
258                        if (pathToNode.length() != 0) {
259                            if (!pathToNode.endsWith("]")) pathToNode = pathToNode + "[1]";
260                            node = pathToNode.equals("") ? root : root.getNode(pathToNode);
261                        }
262    
263                        // Now populate the properties and children ...
264                        if (properties != null) {
265                            for (PropertyIterator iter = node.getProperties(); iter.hasNext();) {
266                                javax.jcr.Property property = iter.nextProperty();
267                                Object[] values = null;
268                                // Must call either 'getValue()' or 'getValues()' depending upon # of values
269                                if (property.getDefinition().isMultiple()) {
270                                    Value[] jcrValues = property.getValues();
271                                    values = new String[jcrValues.length];
272                                    for (int i = 0; i < jcrValues.length; i++) {
273                                        values[i] = jcrValues[i].getString();
274                                    }
275                                } else {
276                                    values = new Object[] {property.getValue().getString()};
277                                }
278                                properties.put(property.getName(), values);
279                            }
280                        }
281                        if (children != null) {
282                            // Figure out which children need same-name sibling indexes ...
283                            Set<String> sameNameSiblings = new HashSet<String>();
284                            for (NodeIterator iter = node.getNodes(); iter.hasNext();) {
285                                javax.jcr.Node child = iter.nextNode();
286                                if (child.getIndex() > 1) sameNameSiblings.add(child.getName());
287                            }
288                            for (NodeIterator iter = node.getNodes(); iter.hasNext();) {
289                                javax.jcr.Node child = iter.nextNode();
290                                String name = child.getName();
291                                if (sameNameSiblings.contains(name)) name = name + "[" + child.getIndex() + "]";
292                                children.add(name);
293                            }
294                        }
295                    } catch (javax.jcr.ItemNotFoundException e) {
296                        return false;
297                    } catch (javax.jcr.PathNotFoundException e) {
298                        return false;
299                    } finally {
300                        if (session != null) session.logout();
301                    }
302                    break;
303                }
304                case DNA: {
305                    try {
306                        // Use the DNA Graph API to read the properties and children of the node ...
307                        ExecutionContext context = this.engine.getExecutionContext();
308                        if (loginContext != null) {
309                            JaasSecurityContext security = new JaasSecurityContext(loginContext);
310                            context = context.with(security);
311                        }
312                        Graph graph = engine.getGraph(context, sourceName);
313                        org.jboss.dna.graph.Node node = graph.getNodeAt(pathToNode);
314    
315                        if (properties != null) {
316                            // Now copy the properties into the map provided as a method parameter ...
317                            for (Property property : node.getProperties()) {
318                                String name = property.getName().getString(context.getNamespaceRegistry());
319                                properties.put(name, property.getValuesAsArray());
320                            }
321                        }
322                        if (children != null) {
323                            // And copy the names of the children into the list provided as a method parameter ...
324                            for (Location child : node.getChildren()) {
325                                String name = child.getPath().getLastSegment().getString(context.getNamespaceRegistry());
326                                children.add(name);
327                            }
328                        }
329                    } catch (PathNotFoundException e) {
330                        return false;
331                    }
332                    break;
333                }
334            }
335            return true;
336        }
337    
338        /**
339         * Utility to build a path given the current path and the input path as string, where the input path could be an absolute path
340         * or relative to the current and where the input may use "." and "..".
341         * 
342         * @param current the current path
343         * @param input the input path
344         * @return the resulting full and normalized path
345         */
346        public String buildPath( String current,
347                                 String input ) {
348            if (current == null) current = "/";
349            if (input == null || input.length() == 0) return current;
350            ExecutionContext context = this.engine.getExecutionContext();
351            PathFactory factory = context.getValueFactories().getPathFactory();
352            Path inputPath = factory.create(input);
353            if (inputPath.isAbsolute()) {
354                return inputPath.getNormalizedPath().getString(context.getNamespaceRegistry(), NoOpEncoder.getInstance());
355            }
356            Path currentPath = factory.create(current);
357            currentPath = factory.create(currentPath, inputPath);
358            currentPath = currentPath.getNormalizedPath();
359            return currentPath.getString(context.getNamespaceRegistry(), NoOpEncoder.getInstance());
360        }
361    
362        /**
363         * A class that represents JCR Credentials containing the JAAS LoginContext.
364         * 
365         * @author Randall Hauch
366         */
367        @Immutable
368        protected static class JaasCredentials implements Credentials {
369            private static final long serialVersionUID = 1L;
370            private final LoginContext context;
371    
372            public JaasCredentials( LoginContext context ) {
373                CheckArg.isNotNull(context, "context");
374                this.context = context;
375            }
376    
377            /**
378             * JBoss DNA's JCR implementation will reflectively look for and call this method to get the JAAS LoginContext.
379             * 
380             * @return the current LoginContext
381             */
382            public LoginContext getLoginContext() {
383                return context;
384            }
385    
386        }
387    }