1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.eclipse.aether.internal.impl.synccontext.named;
20
21 import java.util.Collection;
22 import java.util.stream.Collectors;
23
24 import org.eclipse.aether.RepositorySystemSession;
25 import org.eclipse.aether.artifact.Artifact;
26 import org.eclipse.aether.metadata.Metadata;
27 import org.eclipse.aether.named.NamedLockKey;
28 import org.eclipse.aether.util.ConfigUtils;
29 import org.eclipse.aether.util.StringDigestUtil;
30
31 import static java.util.Objects.requireNonNull;
32
33 /**
34 * Wrapping {@link NameMapper}, that wraps another {@link NameMapper} and hashes resulting strings. It makes use of
35 * fact that (proper) Hash will create unique fixed length string for each different input string (so injection still
36 * stands). This mapper produces file system friendly names. Supports different "depths" (0-4 inclusive) where the
37 * name will contain 0 to 4 level deep directories.
38 * <p>
39 * This mapper is usable in any scenario, but intent was to produce more "compact" name mapper for file locking.
40 *
41 * @since 1.9.0
42 */
43 public class HashingNameMapper implements NameMapper {
44 /**
45 * The depth how many levels should adapter create. Acceptable values are 0-4 (inclusive).
46 *
47 * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
48 * @configurationType {@link java.lang.Integer}
49 * @configurationDefaultValue {@link #DEFAULT_DEPTH}
50 */
51 public static final String CONFIG_PROP_DEPTH = NamedLockFactoryAdapter.CONFIG_PROPS_PREFIX + "hashing.depth";
52
53 public static final int DEFAULT_DEPTH = 2;
54
55 private final NameMapper delegate;
56
57 public HashingNameMapper(final NameMapper delegate) {
58 this.delegate = requireNonNull(delegate);
59 }
60
61 @Override
62 public boolean isFileSystemFriendly() {
63 return true; // hashes delegated strings, so whatever it wrapped, it does not come through
64 }
65
66 @Override
67 public Collection<NamedLockKey> nameLocks(
68 RepositorySystemSession session,
69 Collection<? extends Artifact> artifacts,
70 Collection<? extends Metadata> metadatas) {
71 final int depth = ConfigUtils.getInteger(session, DEFAULT_DEPTH, CONFIG_PROP_DEPTH);
72 if (depth < 0 || depth > 4) {
73 throw new IllegalArgumentException("allowed depth value is between 0 and 4 (inclusive)");
74 }
75 return delegate.nameLocks(session, artifacts, metadatas).stream()
76 .map(k -> NamedLockKey.of(hashName(k.name(), depth), k.resources()))
77 .collect(Collectors.toList());
78 }
79
80 private String hashName(final String name, final int depth) {
81 String hashedName = StringDigestUtil.sha1(name);
82 if (depth == 0) {
83 return hashedName;
84 }
85 StringBuilder prefix = new StringBuilder();
86 int i = 0;
87 while (i < hashedName.length() && i / 2 < depth) {
88 prefix.append(hashedName, i, i + 2).append("/");
89 i += 2;
90 }
91 return prefix.append(hashedName).toString();
92 }
93 }