-added basic http authentication to rss feed servlet -added commons-codec as dependency which is used for decoding the username and password -updated tests git-svn-id: https://svn.apache.org/repos/asf/archiva/trunk@652981 13f79535-47bb-0310-9956-ffa450edef68tags/archiva-r676265
@@ -299,6 +299,11 @@ | |||
<groupId>commons-fileupload</groupId> | |||
<artifactId>commons-fileupload</artifactId> | |||
</dependency> | |||
<dependency> | |||
<groupId>commons-codec</groupId> | |||
<artifactId>commons-codec</artifactId> | |||
<version>1.3</version> | |||
</dependency> | |||
</dependencies> | |||
<build> | |||
<plugins> |
@@ -20,7 +20,10 @@ package org.apache.maven.archiva.web.rss; | |||
*/ | |||
import java.io.IOException; | |||
import java.util.ArrayList; | |||
import java.util.Collections; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import javax.servlet.ServletException; | |||
@@ -28,8 +31,23 @@ import javax.servlet.http.HttpServlet; | |||
import javax.servlet.http.HttpServletRequest; | |||
import javax.servlet.http.HttpServletResponse; | |||
import org.apache.archiva.rss.RssFeedGenerator; | |||
import org.apache.archiva.rss.processor.RssFeedProcessor; | |||
import org.apache.commons.codec.Decoder; | |||
import org.apache.commons.codec.DecoderException; | |||
import org.apache.commons.codec.binary.Base64; | |||
import org.apache.maven.archiva.security.AccessDeniedException; | |||
import org.apache.maven.archiva.security.ArchivaRoleConstants; | |||
import org.apache.maven.archiva.security.ArchivaSecurityException; | |||
import org.apache.maven.archiva.security.PrincipalNotFoundException; | |||
import org.apache.maven.archiva.security.UserRepositories; | |||
import org.codehaus.plexus.redback.authentication.AuthenticationDataSource; | |||
import org.codehaus.plexus.redback.authentication.AuthenticationException; | |||
import org.codehaus.plexus.redback.authentication.PasswordBasedAuthenticationDataSource; | |||
import org.codehaus.plexus.redback.authorization.AuthorizationException; | |||
import org.codehaus.plexus.redback.policy.AccountLockedException; | |||
import org.codehaus.plexus.redback.system.SecuritySession; | |||
import org.codehaus.plexus.redback.system.SecuritySystem; | |||
import org.codehaus.plexus.redback.users.UserNotFoundException; | |||
import org.codehaus.plexus.spring.PlexusToSpringUtils; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
@@ -53,17 +71,29 @@ public class RssFeedServlet | |||
private static final String COULD_NOT_GENERATE_FEED_ERROR = "Could not generate feed"; | |||
private Logger log = LoggerFactory.getLogger( RssFeedGenerator.class ); | |||
private static final String COULD_NOT_AUTHENTICATE_USER = "Could not authenticate user"; | |||
private static final String USER_NOT_AUTHORIZED = "User not authorized to access feed."; | |||
private Logger log = LoggerFactory.getLogger( RssFeedServlet.class ); | |||
private RssFeedProcessor processor; | |||
private WebApplicationContext wac; | |||
private SecuritySystem securitySystem; | |||
private UserRepositories userRepositories; | |||
public void init( javax.servlet.ServletConfig servletConfig ) | |||
throws ServletException | |||
{ | |||
super.init( servletConfig ); | |||
wac = WebApplicationContextUtils.getRequiredWebApplicationContext( servletConfig.getServletContext() ); | |||
securitySystem = | |||
(SecuritySystem) wac.getBean( PlexusToSpringUtils.buildSpringId( SecuritySystem.class.getName() ) ); | |||
userRepositories = | |||
(UserRepositories) wac.getBean( PlexusToSpringUtils.buildSpringId( UserRepositories.class.getName() ) ); | |||
} | |||
public void doGet( HttpServletRequest req, HttpServletResponse res ) | |||
@@ -74,17 +104,21 @@ public class RssFeedServlet | |||
{ | |||
Map<String, String> map = new HashMap<String, String>(); | |||
SyndFeed feed = null; | |||
if ( req.getParameter( "repoId" ) != null ) | |||
String repoId = req.getParameter( "repoId" ); | |||
String groupId = req.getParameter( "groupId" ); | |||
String artifactId = req.getParameter( "artifactId" ); | |||
if ( repoId != null ) | |||
{ | |||
if ( isAuthorized() ) | |||
if ( isAuthorized( req ) ) | |||
{ | |||
// new artifacts in repo feed request | |||
// new artifacts in repo feed request | |||
processor = | |||
(RssFeedProcessor) wac.getBean( PlexusToSpringUtils.buildSpringId( | |||
RssFeedProcessor.class.getName(), | |||
"new-artifacts" ) ); | |||
map.put( RssFeedProcessor.KEY_REPO_ID, req.getParameter( "repoId" ) ); | |||
map.put( RssFeedProcessor.KEY_REPO_ID, repoId ); | |||
} | |||
else | |||
{ | |||
@@ -92,17 +126,17 @@ public class RssFeedServlet | |||
return; | |||
} | |||
} | |||
else if ( ( req.getParameter( "groupId" ) != null ) && ( req.getParameter( "artifactId" ) != null ) ) | |||
else if ( ( groupId != null ) && ( artifactId != null ) ) | |||
{ | |||
if ( isAuthorized() ) | |||
if ( isAuthorized( req ) ) | |||
{ | |||
// new versions of artifact feed request | |||
// new versions of artifact feed request | |||
processor = | |||
(RssFeedProcessor) wac.getBean( PlexusToSpringUtils.buildSpringId( | |||
RssFeedProcessor.class.getName(), | |||
"new-versions" ) ); | |||
map.put( RssFeedProcessor.KEY_GROUP_ID, req.getParameter( "groupId" ) ); | |||
map.put( RssFeedProcessor.KEY_ARTIFACT_ID, req.getParameter( "artifactId" ) ); | |||
map.put( RssFeedProcessor.KEY_GROUP_ID, groupId ); | |||
map.put( RssFeedProcessor.KEY_ARTIFACT_ID, artifactId ); | |||
} | |||
else | |||
{ | |||
@@ -122,16 +156,115 @@ public class RssFeedServlet | |||
SyndFeedOutput output = new SyndFeedOutput(); | |||
output.output( feed, res.getWriter() ); | |||
} | |||
catch ( AuthorizationException ae ) | |||
{ | |||
log.error( USER_NOT_AUTHORIZED, ae ); | |||
res.sendError( HttpServletResponse.SC_UNAUTHORIZED, USER_NOT_AUTHORIZED ); | |||
} | |||
catch ( UserNotFoundException unfe ) | |||
{ | |||
log.error( COULD_NOT_AUTHENTICATE_USER, unfe ); | |||
res.sendError( HttpServletResponse.SC_UNAUTHORIZED, COULD_NOT_AUTHENTICATE_USER ); | |||
} | |||
catch ( AccountLockedException acce ) | |||
{ | |||
log.error( COULD_NOT_AUTHENTICATE_USER, acce ); | |||
res.sendError( HttpServletResponse.SC_UNAUTHORIZED, COULD_NOT_AUTHENTICATE_USER ); | |||
} | |||
catch ( AuthenticationException authe ) | |||
{ | |||
log.error( COULD_NOT_AUTHENTICATE_USER, authe ); | |||
res.sendError( HttpServletResponse.SC_UNAUTHORIZED, COULD_NOT_AUTHENTICATE_USER ); | |||
} | |||
catch ( FeedException ex ) | |||
{ | |||
String msg = COULD_NOT_GENERATE_FEED_ERROR; | |||
log.error( msg, ex ); | |||
res.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg ); | |||
log.error( COULD_NOT_GENERATE_FEED_ERROR, ex ); | |||
res.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, COULD_NOT_GENERATE_FEED_ERROR ); | |||
} | |||
} | |||
private boolean isAuthorized() | |||
/** | |||
* Basic authentication. | |||
* | |||
* @param req | |||
* @return | |||
*/ | |||
private boolean isAuthorized( HttpServletRequest req ) | |||
throws UserNotFoundException, AccountLockedException, AuthenticationException, AuthorizationException | |||
{ | |||
return true; | |||
String auth = req.getHeader( "Authorization" ); | |||
if ( auth == null ) | |||
{ | |||
return false; | |||
} | |||
if ( !auth.toUpperCase().startsWith( "BASIC " ) ) | |||
{ | |||
return false; | |||
} | |||
Decoder dec = new Base64(); | |||
String usernamePassword = ""; | |||
try | |||
{ | |||
usernamePassword = new String( ( byte[] ) dec.decode( auth.substring( 6 ).getBytes() ) ); | |||
} | |||
catch ( DecoderException ie ) | |||
{ | |||
log.error( "Error decoding username and password.", ie.getMessage() ); | |||
} | |||
String[] userCredentials = usernamePassword.split( ":" ); | |||
String username = userCredentials[0]; | |||
String password = userCredentials[1]; | |||
AuthenticationDataSource dataSource = new PasswordBasedAuthenticationDataSource( username, password ); | |||
SecuritySession session = null; | |||
List<String> repoIds = new ArrayList<String>(); | |||
if ( req.getParameter( "repoId" ) != null ) | |||
{ | |||
repoIds.add( req.getParameter( "repoId" ) ); | |||
} | |||
else | |||
{ | |||
repoIds = getObservableRepos( username ); | |||
} | |||
session = securitySystem.authenticate( dataSource ); | |||
for ( String repoId : repoIds ) | |||
{ | |||
if ( securitySystem.isAuthorized( session, ArchivaRoleConstants.OPERATION_REPOSITORY_ACCESS, repoId ) ) | |||
{ | |||
return true; | |||
} | |||
} | |||
return false; | |||
} | |||
private List<String> getObservableRepos( String principal ) | |||
{ | |||
try | |||
{ | |||
return userRepositories.getObservableRepositoryIds( principal ); | |||
} | |||
catch ( PrincipalNotFoundException e ) | |||
{ | |||
log.warn( e.getMessage(), e ); | |||
} | |||
catch ( AccessDeniedException e ) | |||
{ | |||
log.warn( e.getMessage(), e ); | |||
} | |||
catch ( ArchivaSecurityException e ) | |||
{ | |||
log.warn( e.getMessage(), e ); | |||
} | |||
return Collections.emptyList(); | |||
} | |||
} |
@@ -19,9 +19,19 @@ package org.apache.maven.archiva.web.rss; | |||
* under the License. | |||
*/ | |||
import java.io.IOException; | |||
import javax.servlet.http.HttpServletResponse; | |||
import org.apache.commons.codec.Encoder; | |||
import org.apache.commons.codec.binary.Base64; | |||
import org.codehaus.plexus.spring.PlexusInSpringTestCase; | |||
import sun.misc.BASE64Encoder; | |||
import com.meterware.httpunit.GetMethodWebRequest; | |||
import com.meterware.httpunit.HttpException; | |||
import com.meterware.httpunit.WebRequest; | |||
import com.meterware.httpunit.WebResponse; | |||
import com.meterware.servletunit.ServletRunner; | |||
import com.meterware.servletunit.ServletUnitClient; | |||
@@ -60,9 +70,15 @@ public class RssFeedServletTest | |||
(RssFeedServlet) client.newInvocation( "http://localhost/rss/rss_feeds?repoId=test-repo" ).getServlet(); | |||
assertNotNull( servlet ); | |||
WebResponse response = client.getResponse( "http://localhost/rss/rss_feeds?repoId=test-repo" ); | |||
WebRequest request = new GetMethodWebRequest( "http://localhost/rss/rss_feeds?repoId=test-repo" ); | |||
BASE64Encoder encoder = new BASE64Encoder(); | |||
String userPass = "user1:password1"; | |||
String encodedUserPass = encoder.encode( userPass.getBytes() ); | |||
request.setHeaderField( "Authorization", "BASIC " + encodedUserPass ); | |||
WebResponse response = client.getResponse( request ); | |||
assertEquals( RssFeedServlet.MIME_TYPE, response.getHeaderField( "CONTENT-TYPE" ) ); | |||
assertNotNull( "Should have recieved a response", response ); | |||
assertEquals( "Should have been an OK response code.", HttpServletResponse.SC_OK, response.getResponseCode() ); | |||
} | |||
@@ -75,9 +91,15 @@ public class RssFeedServletTest | |||
"http://localhost/rss/rss_feeds?groupId=org.apache.archiva&artifactId=artifact-two" ).getServlet(); | |||
assertNotNull( servlet ); | |||
WebResponse response = client.getResponse( "http://localhost/rss/rss_feeds?groupId=org.apache.archiva&artifactId=artifact-two" ); | |||
WebRequest request = new GetMethodWebRequest( "http://localhost/rss/rss_feeds?groupId=org.apache.archiva&artifactId=artifact-two" ); | |||
BASE64Encoder encoder = new BASE64Encoder(); | |||
String userPass = "user1:password1"; | |||
String encodedUserPass = encoder.encode( userPass.getBytes() ); | |||
request.setHeaderField( "Authorization", "BASIC " + encodedUserPass ); | |||
WebResponse response = client.getResponse( request ); | |||
assertEquals( RssFeedServlet.MIME_TYPE, response.getHeaderField( "CONTENT-TYPE" ) ); | |||
assertNotNull( "Should have recieved a response", response ); | |||
assertEquals( "Should have been an OK response code.", HttpServletResponse.SC_OK, response.getResponseCode() ); | |||
} | |||
@@ -99,20 +121,59 @@ public class RssFeedServletTest | |||
assertEquals( "Should have been a bad request response code.", HttpServletResponse.SC_BAD_REQUEST, he.getResponseCode() ); | |||
} | |||
} | |||
public void testInvalidAuthenticationRequest() | |||
throws Exception | |||
{ | |||
RssFeedServlet servlet = | |||
(RssFeedServlet) client.newInvocation( | |||
"http://localhost/rss/rss_feeds?repoId=unauthorized-repo" ).getServlet(); | |||
assertNotNull( servlet ); | |||
public void testUnAuthorizedRequest() | |||
WebRequest request = new GetMethodWebRequest( "http://localhost/rss/rss_feeds?repoId=unauthorized-repo" ); | |||
Encoder encoder = new Base64(); | |||
String userPass = "unauthUser:unauthPass"; | |||
String encodedUserPass = new String( ( byte[] ) encoder.encode( userPass.getBytes() ) ); | |||
request.setHeaderField( "Authorization", "BASIC " + encodedUserPass ); | |||
try | |||
{ | |||
WebResponse response = client.getResponse( request ); | |||
} | |||
catch ( HttpException he ) | |||
{ | |||
assertEquals( "Should have been a unauthorized response.", HttpServletResponse.SC_UNAUTHORIZED, he.getResponseCode() ); | |||
} | |||
} | |||
public void testUnauthorizedRequest() | |||
throws Exception | |||
{ | |||
RssFeedServlet servlet = | |||
(RssFeedServlet) client.newInvocation( | |||
"http://localhost/rss/rss_feeds" ).getServlet(); | |||
"http://localhost/rss/rss_feeds?repoId=unauthorized-repo" ).getServlet(); | |||
assertNotNull( servlet ); | |||
//WebResponse response = client.getResponse( "http://localhost/rss/rss_feeds" ); | |||
//assertNotNull( "Should have recieved a response", response ); | |||
//assertEquals( "Should have been a bad request response code.", HttpServletResponse.SC_BAD_REQUEST, response.getResponseCode() ); | |||
WebRequest request = new GetMethodWebRequest( "http://localhost/rss/rss_feeds?repoId=unauthorized-repo" ); | |||
BASE64Encoder encoder = new BASE64Encoder(); | |||
String userPass = "user1:password1"; | |||
String encodedUserPass = encoder.encode( userPass.getBytes() ); | |||
request.setHeaderField( "Authorization", "BASIC " + encodedUserPass ); | |||
try | |||
{ | |||
WebResponse response = client.getResponse( request ); | |||
} | |||
catch ( HttpException he ) | |||
{ | |||
assertEquals( "Should have been a unauthorized response.", HttpServletResponse.SC_UNAUTHORIZED, he.getResponseCode() ); | |||
} | |||
} | |||
@Override | |||
protected String getPlexusConfigLocation() | |||
{ |
@@ -0,0 +1,164 @@ | |||
package org.apache.maven.archiva.web.rss; | |||
/* | |||
* Licensed to the Apache Software Foundation (ASF) under one | |||
* or more contributor license agreements. See the NOTICE file | |||
* distributed with this work for additional information | |||
* regarding copyright ownership. The ASF licenses this file | |||
* to you under the Apache License, Version 2.0 (the | |||
* "License"); you may not use this file except in compliance | |||
* with the License. You may obtain a copy of the License at | |||
* | |||
* http://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, | |||
* software distributed under the License is distributed on an | |||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |||
* KIND, either express or implied. See the License for the | |||
* specific language governing permissions and limitations | |||
* under the License. | |||
*/ | |||
import java.util.ArrayList; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import org.codehaus.plexus.redback.authentication.AuthenticationDataSource; | |||
import org.codehaus.plexus.redback.authentication.AuthenticationException; | |||
import org.codehaus.plexus.redback.authentication.AuthenticationResult; | |||
import org.codehaus.plexus.redback.authorization.AuthorizationException; | |||
import org.codehaus.plexus.redback.authorization.AuthorizationResult; | |||
import org.codehaus.plexus.redback.keys.KeyManager; | |||
import org.codehaus.plexus.redback.policy.AccountLockedException; | |||
import org.codehaus.plexus.redback.policy.UserSecurityPolicy; | |||
import org.codehaus.plexus.redback.system.DefaultSecuritySession; | |||
import org.codehaus.plexus.redback.system.SecuritySession; | |||
import org.codehaus.plexus.redback.system.SecuritySystem; | |||
import org.codehaus.plexus.redback.users.User; | |||
import org.codehaus.plexus.redback.users.UserManager; | |||
import org.codehaus.plexus.redback.users.UserNotFoundException; | |||
import org.codehaus.plexus.redback.users.jdo.JdoUser; | |||
/** | |||
* SecuritySystem stub used for testing. | |||
* | |||
* @author <a href="mailto:oching@apache.org">Maria Odea Ching</a> | |||
* @version $Id$ | |||
*/ | |||
public class SecuritySystemStub | |||
implements SecuritySystem | |||
{ | |||
Map<String, String> users = new HashMap<String, String>(); | |||
List<String> repoIds = new ArrayList<String>(); | |||
public SecuritySystemStub() | |||
{ | |||
users.put( "user1", "password1" ); | |||
users.put( "user2", "password2" ); | |||
users.put( "user3", "password3" ); | |||
repoIds.add( "test-repo" ); | |||
} | |||
public SecuritySession authenticate( AuthenticationDataSource source ) | |||
throws AuthenticationException, UserNotFoundException, AccountLockedException | |||
{ | |||
AuthenticationResult result = null; | |||
SecuritySession session = null; | |||
if ( users.get( source.getPrincipal() ) != null ) | |||
{ | |||
result = new AuthenticationResult( true, source.getPrincipal(), null ); | |||
User user = new JdoUser(); | |||
user.setUsername( source.getPrincipal() ); | |||
user.setPassword( users.get( source.getPrincipal() ) ); | |||
session = new DefaultSecuritySession( result, user ); | |||
} | |||
else | |||
{ | |||
result = new AuthenticationResult( false, source.getPrincipal(), null ); | |||
session = new DefaultSecuritySession( result ); | |||
} | |||
return session; | |||
} | |||
public AuthorizationResult authorize( SecuritySession arg0, Object arg1 ) | |||
throws AuthorizationException | |||
{ | |||
// TODO Auto-generated method stub | |||
return null; | |||
} | |||
public AuthorizationResult authorize( SecuritySession arg0, Object arg1, Object arg2 ) | |||
throws AuthorizationException | |||
{ | |||
// TODO Auto-generated method stub | |||
return null; | |||
} | |||
public String getAuthenticatorId() | |||
{ | |||
// TODO Auto-generated method stub | |||
return null; | |||
} | |||
public String getAuthorizerId() | |||
{ | |||
// TODO Auto-generated method stub | |||
return null; | |||
} | |||
public KeyManager getKeyManager() | |||
{ | |||
// TODO Auto-generated method stub | |||
return null; | |||
} | |||
public UserSecurityPolicy getPolicy() | |||
{ | |||
// TODO Auto-generated method stub | |||
return null; | |||
} | |||
public String getUserManagementId() | |||
{ | |||
// TODO Auto-generated method stub | |||
return null; | |||
} | |||
public UserManager getUserManager() | |||
{ | |||
// TODO Auto-generated method stub | |||
return null; | |||
} | |||
public boolean isAuthenticated( AuthenticationDataSource arg0 ) | |||
throws AuthenticationException, UserNotFoundException, AccountLockedException | |||
{ | |||
// TODO Auto-generated method stub | |||
return false; | |||
} | |||
public boolean isAuthorized( SecuritySession arg0, Object arg1 ) | |||
throws AuthorizationException | |||
{ | |||
// TODO Auto-generated method stub | |||
return false; | |||
} | |||
public boolean isAuthorized( SecuritySession arg0, Object arg1, Object arg2 ) | |||
throws AuthorizationException | |||
{ | |||
if ( repoIds.contains( arg2 ) ) | |||
{ | |||
return true; | |||
} | |||
return false; | |||
} | |||
} |
@@ -0,0 +1,63 @@ | |||
package org.apache.maven.archiva.web.rss; | |||
/* | |||
* Licensed to the Apache Software Foundation (ASF) under one | |||
* or more contributor license agreements. See the NOTICE file | |||
* distributed with this work for additional information | |||
* regarding copyright ownership. The ASF licenses this file | |||
* to you under the Apache License, Version 2.0 (the | |||
* "License"); you may not use this file except in compliance | |||
* with the License. You may obtain a copy of the License at | |||
* | |||
* http://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, | |||
* software distributed under the License is distributed on an | |||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |||
* KIND, either express or implied. See the License for the | |||
* specific language governing permissions and limitations | |||
* under the License. | |||
*/ | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import org.apache.maven.archiva.security.AccessDeniedException; | |||
import org.apache.maven.archiva.security.ArchivaSecurityException; | |||
import org.apache.maven.archiva.security.PrincipalNotFoundException; | |||
import org.apache.maven.archiva.security.UserRepositories; | |||
/** | |||
* UserRepositories stub used for testing. | |||
* | |||
* @author <a href="mailto:oching@apache.org">Maria Odea Ching</a> | |||
* @version $Id$ | |||
*/ | |||
public class UserRepositoriesStub | |||
implements UserRepositories | |||
{ | |||
public void createMissingRepositoryRoles( String repoId ) | |||
throws ArchivaSecurityException | |||
{ | |||
// TODO Auto-generated method stub | |||
} | |||
public List<String> getObservableRepositoryIds( String principal ) | |||
throws PrincipalNotFoundException, AccessDeniedException, ArchivaSecurityException | |||
{ | |||
List<String> repoIds = new ArrayList<String>(); | |||
repoIds.add( "test-repo" ); | |||
return repoIds; | |||
} | |||
public boolean isAuthorizedToUploadArtifacts( String principal, String repoId ) | |||
throws PrincipalNotFoundException, ArchivaSecurityException | |||
{ | |||
// TODO Auto-generated method stub | |||
return false; | |||
} | |||
} |
@@ -34,5 +34,17 @@ | |||
<role-hint>jdo</role-hint> | |||
<implementation>org.apache.maven.archiva.web.rss.ArtifactDAOStub</implementation> | |||
</component> | |||
<component> | |||
<role>org.codehaus.plexus.redback.system.SecuritySystem</role> | |||
<role-hint>default</role-hint> | |||
<implementation>org.apache.maven.archiva.web.rss.SecuritySystemStub</implementation> | |||
</component> | |||
<component> | |||
<role>org.apache.maven.archiva.security.UserRepositories</role> | |||
<role-hint>default</role-hint> | |||
<implementation>org.apache.maven.archiva.web.rss.UserRepositoriesStub</implementation> | |||
</component> | |||
</components> | |||
</plexus> |