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.dna.maven;
025    
026    import java.net.MalformedURLException;
027    import java.net.URL;
028    import java.net.URLStreamHandler;
029    import java.util.regex.Matcher;
030    import java.util.regex.Pattern;
031    import org.jboss.dna.common.text.TextDecoder;
032    import org.jboss.dna.common.text.TextEncoder;
033    import org.jboss.dna.common.text.UrlEncoder;
034    
035    /**
036     * Wrapper for a URL that uses a format for referencing JCR nodes and content.
037     * 
038     * @author Randall Hauch
039     */
040    public class MavenUrl {
041    
042        public static final int NO_PORT = -1;
043        public static final String JCR_PROTOCOL = "jcr";
044        protected static final String URL_PATH_DELIMITER = "/";
045    
046        private String hostname = "";
047        private int port = NO_PORT;
048        private String workspaceName = "";
049        private String path = URL_PATH_DELIMITER;
050    
051        /**
052         * Get the host name
053         * 
054         * @return the host name
055         */
056        public String getHostname() {
057            return this.hostname;
058        }
059    
060        /**
061         * @param hostname the new host name
062         */
063        public void setHostname( String hostname ) {
064            this.hostname = hostname != null ? hostname.trim() : "";
065            this.hostname = trimDelimiters(this.hostname, true, true);
066        }
067    
068        /**
069         * Get the port. This method returns {@link #NO_PORT} if the port has not been specified.
070         * 
071         * @return the port
072         */
073        public int getPort() {
074            return this.port;
075        }
076    
077        /**
078         * @param port the new port, or {@link #NO_PORT} if there is no port
079         */
080        public void setPort( int port ) {
081            this.port = port;
082        }
083    
084        public String getHostnameAndPort() {
085            if (this.port == NO_PORT) return this.hostname;
086            if (this.hostname.length() == 0) return "";
087            return this.hostname + ":" + this.port;
088        }
089    
090        /**
091         * @return workspaceName
092         */
093        public String getWorkspaceName() {
094            return this.workspaceName;
095        }
096    
097        /**
098         * Set the name of the workspace.
099         * 
100         * @param workspaceName the name of the workspace
101         */
102        public void setWorkspaceName( String workspaceName ) {
103            this.workspaceName = workspaceName != null ? workspaceName.trim() : "";
104            this.workspaceName = trimDelimiters(this.workspaceName, true, true);
105        }
106    
107        protected String trimDelimiters( String string, boolean removeLeading, boolean removeTrailing ) {
108            if (string == null || string.length() == 0) return "";
109            if (removeLeading) string = string.replaceAll("^/+", "");
110            if (removeTrailing) string = string.replaceAll("/+$", "");
111            return string;
112        }
113    
114        /**
115         * @return path
116         */
117        public String getPath() {
118            return this.path;
119        }
120    
121        /**
122         * @param path Sets path to the specified value.
123         */
124        public void setPath( String path ) {
125            // Make sure the path starts with a '/' ...
126            this.path = path != null ? URL_PATH_DELIMITER + path.trim() : URL_PATH_DELIMITER;
127            this.path = this.path.replaceAll("^/{2,}", "/");
128            assert this.path.startsWith(URL_PATH_DELIMITER);
129        }
130    
131        /**
132         * Get a URL that corresponds to the information in this object.
133         * 
134         * @param handler the URL stream handler that will be used to handle obtaining an input stream or an output stream on the
135         * resulting URL
136         * @param encoder an encoder that will be used to escape any characters that are not allowed in URLs; {@link UrlEncoder} will
137         * be used if no encoder is specified
138         * @return the URL
139         * @throws MalformedURLException if the resulting URL would be malformed
140         */
141        public URL getUrl( URLStreamHandler handler, TextEncoder encoder ) throws MalformedURLException {
142            if (encoder == null) {
143                encoder = new UrlEncoder().setSlashEncoded(false);
144            }
145            final boolean hasWorkspaceName = this.workspaceName.length() > 0;
146            final boolean hasPath = this.path.length() > 1; // path includes leading delim
147            String filePart = null;
148            if (hasWorkspaceName && hasPath) {
149                filePart = URL_PATH_DELIMITER + encoder.encode(this.workspaceName) + encoder.encode(this.path);
150            } else if (hasWorkspaceName) {
151                filePart = URL_PATH_DELIMITER + encoder.encode(this.workspaceName) + URL_PATH_DELIMITER;
152            } else if (hasPath) {
153                filePart = URL_PATH_DELIMITER + encoder.encode(this.path);
154            } else {
155                filePart = URL_PATH_DELIMITER;
156            }
157            int actualPort = this.hostname.length() != 0 ? this.port : NO_PORT;
158            return new URL(JCR_PROTOCOL, this.hostname, actualPort, filePart, handler);
159        }
160    
161        /**
162         * {@inheritDoc}
163         */
164        @Override
165        public String toString() {
166            UrlEncoder encoder = new UrlEncoder().setSlashEncoded(false);
167            String encodedWorkspace = encoder.encode(this.workspaceName);
168            String encodedPath = encoder.encode(this.path);
169            final boolean hasHostname = this.hostname.length() > 0;
170            final boolean hasPath = encodedPath.length() > 1; // path includes leading delim
171            StringBuilder sb = new StringBuilder();
172            sb.append(JCR_PROTOCOL).append(":");
173            if (hasHostname) {
174                sb.append("//").append(this.hostname);
175                if (this.port != NO_PORT) {
176                    sb.append(":").append(this.port);
177                }
178            }
179            sb.append(URL_PATH_DELIMITER).append(encodedWorkspace);
180            if (hasPath) {
181                sb.append(encodedPath);
182            } else {
183                sb.append(URL_PATH_DELIMITER);
184            }
185            return sb.toString();
186        }
187    
188        /**
189         * Parse the supplied URL and determine if the URL fits the JCR URL format. If it does, return a {@link MavenUrl} instance;
190         * otherwise return null. If the URL is malformed or otherwise invalid, this method also returns null.
191         * <p>
192         * The URL format is expected to fit the following pattern:
193         * 
194         * <pre>
195         *    jcr://hostname:port/workspaceName/path/to/node
196         * </pre>
197         * 
198         * where
199         * <ul>
200         * <li><b>hostname</b> is the name of the repository's host; typically, this is unspecified to refer to a repository in the
201         * same VM</li>
202         * <li><b>port</b> is the port on the host. If the hostname is unspecified, the port should be excluded.</li>
203         * <li><b>workspaceName</b> is the name of the workspace in the repository</li>
204         * <li><b>path/to/node</b> is the path of the node or property that is to be referenced</li>
205         * </ul>
206         * </p>
207         * 
208         * @param url the URL to be parsed
209         * @param decoder the text encoder that should be used to decode the URL; may be null if no decoding should be done
210         * @return the object representing the JCR information contained in the URL
211         * @see #parse(URL, TextDecoder)
212         */
213        public static MavenUrl parse( String url, TextDecoder decoder ) {
214            if (decoder == null) decoder = new UrlEncoder();
215            // This regular expression has the following groups:
216            // 1) //hostname:port
217            // 2) hostname:port
218            // 3) hostname
219            // 4) :port
220            // 5) port
221            // 6) workspaceName
222            // 7) path, including leading '/'
223            Pattern urlPattern = Pattern.compile("jcr:(//(([^/:]*)(:([^/]*))?))?/([^/]*)(/?.*)");
224            Matcher matcher = urlPattern.matcher(url);
225            MavenUrl result = null;
226            if (matcher.find()) {
227                result = new MavenUrl();
228                result.setHostname(matcher.group(3));
229                String portStr = matcher.group(5);
230                if (portStr != null && portStr.trim().length() != 0) {
231                    result.setPort(Integer.parseInt(portStr));
232                }
233                String workspaceName = decoder.decode(matcher.group(6));
234                String path = decoder.decode(matcher.group(7));
235                result.setWorkspaceName(workspaceName);
236                result.setPath(path);
237            }
238            return result;
239        }
240    
241        /**
242         * Parse the supplied URL and determine if the URL fits the JCR URL format. If it does, return a {@link MavenUrl} instance;
243         * otherwise return null. If the URL is malformed or otherwise invalid, this method also returns null.
244         * 
245         * @param url the URL to be parsed
246         * @param decoder the text encoder that should be used to decode the URL; may be null if no decoding should be done
247         * @return the object representing the JCR information contained in the URL
248         * @see #parse(String,TextDecoder)
249         */
250        public static MavenUrl parse( URL url, TextDecoder decoder ) {
251            if (url == null) return null;
252            return parse(url.toExternalForm(), decoder);
253        }
254    }