001package org.eclipse.aether.internal.impl.filter; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import javax.inject.Inject; 023import javax.inject.Named; 024import javax.inject.Singleton; 025 026import java.io.BufferedReader; 027import java.io.FileNotFoundException; 028import java.io.IOException; 029import java.io.UncheckedIOException; 030import java.nio.charset.StandardCharsets; 031import java.nio.file.Files; 032import java.nio.file.Path; 033import java.util.ArrayList; 034import java.util.Arrays; 035import java.util.HashMap; 036import java.util.List; 037import java.util.concurrent.ConcurrentHashMap; 038 039import org.eclipse.aether.RepositorySystemSession; 040import org.eclipse.aether.artifact.Artifact; 041import org.eclipse.aether.metadata.Metadata; 042import org.eclipse.aether.repository.RemoteRepository; 043import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter; 044import org.eclipse.aether.spi.connector.layout.RepositoryLayout; 045import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider; 046import org.eclipse.aether.transfer.NoRepositoryLayoutException; 047import org.slf4j.Logger; 048import org.slf4j.LoggerFactory; 049 050import static java.util.Objects.requireNonNull; 051import static java.util.stream.Collectors.toList; 052 053/** 054 * Remote repository filter source filtering on path prefixes. It is backed by a file that lists all allowed path 055 * prefixes from remote repository. Artifact that layout converted path (using remote repository layout) results in 056 * path with no corresponding prefix present in this file is filtered out. 057 * <p> 058 * The file can be authored manually: format is one prefix per line, comments starting with "#" (hash) and empty lines 059 * for structuring are supported, The "/" (slash) character is used as file separator. Some remote repositories and 060 * MRMs publish these kind of files, they can be downloaded from corresponding URLs. 061 * <p> 062 * The prefix file is expected on path "${basedir}/prefixes-${repository.id}.txt". 063 * <p> 064 * The prefixes file is once loaded and cached, so in-flight prefixes file change during component existence are not 065 * noticed. 066 * <p> 067 * Examples of published prefix files: 068 * <ul> 069 * <li>Central: <a href="https://repo.maven.apache.org/maven2/.meta/prefixes.txt">prefixes.txt</a></li> 070 * <li>Apache Releases: 071 * <a href="https://repository.apache.org/content/repositories/releases/.meta/prefixes.txt">prefixes.txt</a></li> 072 * </ul> 073 * 074 * @since 1.9.0 075 */ 076@Singleton 077@Named( PrefixesRemoteRepositoryFilterSource.NAME ) 078public final class PrefixesRemoteRepositoryFilterSource 079 extends RemoteRepositoryFilterSourceSupport 080{ 081 public static final String NAME = "prefixes"; 082 083 static final String PREFIXES_FILE_PREFIX = "prefixes-"; 084 085 static final String PREFIXES_FILE_SUFFIX = ".txt"; 086 087 private static final Logger LOGGER = LoggerFactory.getLogger( PrefixesRemoteRepositoryFilterSource.class ); 088 089 private final RepositoryLayoutProvider repositoryLayoutProvider; 090 091 private final ConcurrentHashMap<RemoteRepository, Node> prefixes; 092 093 private final ConcurrentHashMap<RemoteRepository, RepositoryLayout> layouts; 094 095 @Inject 096 public PrefixesRemoteRepositoryFilterSource( RepositoryLayoutProvider repositoryLayoutProvider ) 097 { 098 super( NAME ); 099 this.repositoryLayoutProvider = requireNonNull( repositoryLayoutProvider ); 100 this.prefixes = new ConcurrentHashMap<>(); 101 this.layouts = new ConcurrentHashMap<>(); 102 } 103 104 @Override 105 public RemoteRepositoryFilter getRemoteRepositoryFilter( RepositorySystemSession session ) 106 { 107 if ( isEnabled( session ) ) 108 { 109 return new PrefixesFilter( session, getBasedir( session, false ) ); 110 } 111 return null; 112 } 113 114 /** 115 * Caches layout instances for remote repository. In case of unknown layout it returns {@code null}. 116 * 117 * @return the layout instance of {@code null} if layout not supported. 118 */ 119 private RepositoryLayout cacheLayout( RepositorySystemSession session, RemoteRepository remoteRepository ) 120 { 121 return layouts.computeIfAbsent( remoteRepository, r -> 122 { 123 try 124 { 125 return repositoryLayoutProvider.newRepositoryLayout( session, remoteRepository ); 126 } 127 catch ( NoRepositoryLayoutException e ) 128 { 129 return null; 130 } 131 } ); 132 } 133 134 /** 135 * Caches prefixes instances for remote repository. 136 */ 137 private Node cacheNode( Path basedir, 138 RemoteRepository remoteRepository ) 139 { 140 return prefixes.computeIfAbsent( remoteRepository, r -> loadRepositoryPrefixes( basedir, remoteRepository ) ); 141 } 142 143 /** 144 * Loads prefixes file and preprocesses it into {@link Node} instance. 145 */ 146 private Node loadRepositoryPrefixes( Path baseDir, RemoteRepository remoteRepository ) 147 { 148 Path filePath = baseDir.resolve( PREFIXES_FILE_PREFIX + remoteRepository.getId() + PREFIXES_FILE_SUFFIX ); 149 if ( Files.isReadable( filePath ) ) 150 { 151 try ( BufferedReader reader = Files.newBufferedReader( filePath, StandardCharsets.UTF_8 ) ) 152 { 153 LOGGER.debug( "Loading prefixes for remote repository {} from file '{}'", remoteRepository.getId(), 154 filePath ); 155 Node root = new Node( "" ); 156 String prefix; 157 int lines = 0; 158 while ( ( prefix = reader.readLine() ) != null ) 159 { 160 if ( !prefix.startsWith( "#" ) && !prefix.trim().isEmpty() ) 161 { 162 lines++; 163 Node currentNode = root; 164 for ( String element : elementsOf( prefix ) ) 165 { 166 currentNode = currentNode.addSibling( element ); 167 } 168 } 169 } 170 LOGGER.info( "Loaded {} prefixes for remote repository {}", lines, remoteRepository.getId() ); 171 return root; 172 } 173 catch ( FileNotFoundException e ) 174 { 175 // strange: we tested for it above, still, we should not fail 176 } 177 catch ( IOException e ) 178 { 179 throw new UncheckedIOException( e ); 180 } 181 } 182 LOGGER.debug( "Prefix file for remote repository {} not found at '{}'", remoteRepository, filePath ); 183 return NOT_PRESENT_NODE; 184 } 185 186 private class PrefixesFilter implements RemoteRepositoryFilter 187 { 188 private final RepositorySystemSession session; 189 190 private final Path basedir; 191 192 private PrefixesFilter( RepositorySystemSession session, Path basedir ) 193 { 194 this.session = session; 195 this.basedir = basedir; 196 } 197 198 @Override 199 public Result acceptArtifact( RemoteRepository remoteRepository, Artifact artifact ) 200 { 201 RepositoryLayout repositoryLayout = cacheLayout( session, remoteRepository ); 202 if ( repositoryLayout == null ) 203 { 204 return new SimpleResult( true, "Unsupported layout: " + remoteRepository ); 205 } 206 return acceptPrefix( remoteRepository, 207 repositoryLayout.getLocation( artifact, false ).getPath() ); 208 } 209 210 @Override 211 public Result acceptMetadata( RemoteRepository remoteRepository, Metadata metadata ) 212 { 213 RepositoryLayout repositoryLayout = cacheLayout( session, remoteRepository ); 214 if ( repositoryLayout == null ) 215 { 216 return new SimpleResult( true, "Unsupported layout: " + remoteRepository ); 217 } 218 return acceptPrefix( remoteRepository, 219 repositoryLayout.getLocation( metadata, false ).getPath() ); 220 } 221 222 private Result acceptPrefix( RemoteRepository remoteRepository, String path ) 223 { 224 Node root = cacheNode( basedir, remoteRepository ); 225 if ( NOT_PRESENT_NODE == root ) 226 { 227 return NOT_PRESENT_RESULT; 228 } 229 List<String> prefix = new ArrayList<>(); 230 final List<String> pathElements = elementsOf( path ); 231 Node currentNode = root; 232 for ( String pathElement : pathElements ) 233 { 234 prefix.add( pathElement ); 235 currentNode = currentNode.getSibling( pathElement ); 236 if ( currentNode == null || currentNode.isLeaf() ) 237 { 238 break; 239 } 240 } 241 if ( currentNode != null && currentNode.isLeaf() ) 242 { 243 return new SimpleResult( true, "Prefix " 244 + String.join( "/", prefix ) + " allowed from " + remoteRepository ); 245 } 246 else 247 { 248 return new SimpleResult( false, "Prefix " 249 + String.join( "/", prefix ) + " NOT allowed from " + remoteRepository ); 250 } 251 } 252 } 253 254 private static final Node NOT_PRESENT_NODE = new Node( 255 "not-present-node" ); 256 257 private static final RemoteRepositoryFilter.Result NOT_PRESENT_RESULT = new SimpleResult( 258 true, "Prefix file not present" ); 259 260 private static class Node 261 { 262 private final String name; 263 264 private final HashMap<String, Node> siblings; 265 266 private Node( String name ) 267 { 268 this.name = name; 269 this.siblings = new HashMap<>(); 270 } 271 272 public String getName() 273 { 274 return name; 275 } 276 277 public boolean isLeaf() 278 { 279 return siblings.isEmpty(); 280 } 281 282 public Node addSibling( String name ) 283 { 284 Node sibling = siblings.get( name ); 285 if ( sibling == null ) 286 { 287 sibling = new Node( name ); 288 siblings.put( name, sibling ); 289 } 290 return sibling; 291 } 292 293 public Node getSibling( String name ) 294 { 295 return siblings.get( name ); 296 } 297 } 298 299 private static List<String> elementsOf( final String path ) 300 { 301 return Arrays.stream( path.split( "/" ) ).filter( e -> e != null && !e.isEmpty() ).collect( toList() ); 302 } 303}