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. 116 */ 117 private RepositoryLayout cacheLayout( RepositorySystemSession session, RemoteRepository remoteRepository ) 118 { 119 return layouts.computeIfAbsent( remoteRepository, r -> 120 { 121 try 122 { 123 return repositoryLayoutProvider.newRepositoryLayout( session, remoteRepository ); 124 } 125 catch ( NoRepositoryLayoutException e ) 126 { 127 throw new RuntimeException( e ); 128 } 129 } ); 130 } 131 132 /** 133 * Caches prefixes instances for remote repository. 134 */ 135 private Node cacheNode( Path basedir, 136 RemoteRepository remoteRepository ) 137 { 138 return prefixes.computeIfAbsent( remoteRepository, r -> loadRepositoryPrefixes( basedir, remoteRepository ) ); 139 } 140 141 /** 142 * Loads prefixes file and preprocesses it into {@link Node} instance. 143 */ 144 private Node loadRepositoryPrefixes( Path baseDir, RemoteRepository remoteRepository ) 145 { 146 Path filePath = baseDir.resolve( PREFIXES_FILE_PREFIX + remoteRepository.getId() + PREFIXES_FILE_SUFFIX ); 147 if ( Files.isReadable( filePath ) ) 148 { 149 try ( BufferedReader reader = Files.newBufferedReader( filePath, StandardCharsets.UTF_8 ) ) 150 { 151 LOGGER.debug( "Loading prefixes for remote repository {} from file '{}'", remoteRepository.getId(), 152 filePath ); 153 Node root = new Node( "" ); 154 String prefix; 155 int lines = 0; 156 while ( ( prefix = reader.readLine() ) != null ) 157 { 158 if ( !prefix.startsWith( "#" ) && !prefix.trim().isEmpty() ) 159 { 160 lines++; 161 Node currentNode = root; 162 for ( String element : elementsOf( prefix ) ) 163 { 164 currentNode = currentNode.addSibling( element ); 165 } 166 } 167 } 168 LOGGER.info( "Loaded {} prefixes for remote repository {}", lines, remoteRepository.getId() ); 169 return root; 170 } 171 catch ( FileNotFoundException e ) 172 { 173 // strange: we tested for it above, still, we should not fail 174 } 175 catch ( IOException e ) 176 { 177 throw new UncheckedIOException( e ); 178 } 179 } 180 LOGGER.debug( "Prefix file for remote repository {} not found at '{}'", remoteRepository, filePath ); 181 return NOT_PRESENT_NODE; 182 } 183 184 private class PrefixesFilter implements RemoteRepositoryFilter 185 { 186 private final RepositorySystemSession session; 187 188 private final Path basedir; 189 190 private PrefixesFilter( RepositorySystemSession session, Path basedir ) 191 { 192 this.session = session; 193 this.basedir = basedir; 194 } 195 196 @Override 197 public Result acceptArtifact( RemoteRepository remoteRepository, Artifact artifact ) 198 { 199 return acceptPrefix( remoteRepository, 200 cacheLayout( session, remoteRepository ).getLocation( artifact, false ).getPath() ); 201 } 202 203 @Override 204 public Result acceptMetadata( RemoteRepository remoteRepository, Metadata metadata ) 205 { 206 return acceptPrefix( remoteRepository, 207 cacheLayout( session, remoteRepository ).getLocation( metadata, false ).getPath() ); 208 } 209 210 private Result acceptPrefix( RemoteRepository remoteRepository, String path ) 211 { 212 Node root = cacheNode( basedir, remoteRepository ); 213 if ( NOT_PRESENT_NODE == root ) 214 { 215 return NOT_PRESENT_RESULT; 216 } 217 List<String> prefix = new ArrayList<>(); 218 final List<String> pathElements = elementsOf( path ); 219 Node currentNode = root; 220 for ( String pathElement : pathElements ) 221 { 222 prefix.add( pathElement ); 223 currentNode = currentNode.getSibling( pathElement ); 224 if ( currentNode == null || currentNode.isLeaf() ) 225 { 226 break; 227 } 228 } 229 if ( currentNode != null && currentNode.isLeaf() ) 230 { 231 return new SimpleResult( true, "Prefix " 232 + String.join( "/", prefix ) + " allowed from " + remoteRepository ); 233 } 234 else 235 { 236 return new SimpleResult( false, "Prefix " 237 + String.join( "/", prefix ) + " NOT allowed from " + remoteRepository ); 238 } 239 } 240 } 241 242 private static final Node NOT_PRESENT_NODE = new Node( 243 "not-present-node" ); 244 245 private static final RemoteRepositoryFilter.Result NOT_PRESENT_RESULT = new SimpleResult( 246 true, "Prefix file not present" ); 247 248 private static class Node 249 { 250 private final String name; 251 252 private final HashMap<String, Node> siblings; 253 254 private Node( String name ) 255 { 256 this.name = name; 257 this.siblings = new HashMap<>(); 258 } 259 260 public String getName() 261 { 262 return name; 263 } 264 265 public boolean isLeaf() 266 { 267 return siblings.isEmpty(); 268 } 269 270 public Node addSibling( String name ) 271 { 272 Node sibling = siblings.get( name ); 273 if ( sibling == null ) 274 { 275 sibling = new Node( name ); 276 siblings.put( name, sibling ); 277 } 278 return sibling; 279 } 280 281 public Node getSibling( String name ) 282 { 283 return siblings.get( name ); 284 } 285 } 286 287 private static List<String> elementsOf( final String path ) 288 { 289 return Arrays.stream( path.split( "/" ) ).filter( e -> e != null && !e.isEmpty() ).collect( toList() ); 290 } 291}