001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.repository;
020
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Collections;
024import java.util.List;
025import java.util.Objects;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028
029import static java.util.Objects.requireNonNull;
030
031/**
032 * A repository on a remote server.
033 * <p>
034 * If use of instances of this class are meant to be used as keys, see {@link #toBareRemoteRepository()} method.
035 */
036public final class RemoteRepository implements ArtifactRepository {
037    /**
038     * The intent this repository is to be used for. Newly created repositories are usually "bare", and before
039     * their actual use (caller or repository system) adapts them, equip with auth/proxy info and even mirrors, if
040     * environment is configured for them. Note: "bare" does not always mean "without authentication", as client
041     * code may create with all required properties, but {@link org.eclipse.aether.RepositorySystem} will process
042     * them anyway, marking their "intent".
043     * <p>
044     * <em>Important consequence:</em> the change of {@link Intent} on repository may affect the use cases when
045     * they are used as keys (they are suitable for that). To use {@link RemoteRepository} instances as key,
046     * you should use instances returned by method {@link #toBareRemoteRepository()}, that returns "normalized"
047     * repository instances usable as keys. Also, in "key usage case" two instances of remote repository are
048     * considered equal if following stands: {@code Objects.equals(r1.toBareRemoteRepository(), r2.toBareRemoteRepository())}.
049     *
050     * @see org.eclipse.aether.RepositorySystem#newResolutionRepositories(org.eclipse.aether.RepositorySystemSession, List)
051     * @see org.eclipse.aether.RepositorySystem#newDeploymentRepository(org.eclipse.aether.RepositorySystemSession, RemoteRepository)
052     * @since 2.0.14
053     */
054    public enum Intent {
055        /** The repository is newly created and not yet adapted for a particular use. */
056        BARE,
057        /** The repository is used to resolve artifacts or metadata. */
058        RESOLUTION,
059        /** The repository is used to deploy artifacts or metadata. */
060        DEPLOYMENT
061    }
062
063    private static final Pattern URL_PATTERN =
064            Pattern.compile("([^:/]+(:[^:/]{2,}+(?=://))?):(//([^@/]*@)?([^/:]+))?.*");
065
066    private final String id;
067
068    private final String type;
069
070    private final String url;
071
072    private final String host;
073
074    private final String protocol;
075
076    private final RepositoryPolicy releasePolicy;
077
078    private final RepositoryPolicy snapshotPolicy;
079
080    private final Proxy proxy;
081
082    private final Authentication authentication;
083
084    private final List<RemoteRepository> mirroredRepositories;
085
086    private final boolean repositoryManager;
087
088    private final boolean blocked;
089
090    private final Intent intent;
091
092    private final int hashCode;
093
094    RemoteRepository(Builder builder) {
095        if (builder.prototype != null) {
096            id = (builder.delta & Builder.ID) != 0 ? builder.id : builder.prototype.id;
097            type = (builder.delta & Builder.TYPE) != 0 ? builder.type : builder.prototype.type;
098            url = (builder.delta & Builder.URL) != 0 ? builder.url : builder.prototype.url;
099            releasePolicy =
100                    (builder.delta & Builder.RELEASES) != 0 ? builder.releasePolicy : builder.prototype.releasePolicy;
101            snapshotPolicy = (builder.delta & Builder.SNAPSHOTS) != 0
102                    ? builder.snapshotPolicy
103                    : builder.prototype.snapshotPolicy;
104            proxy = (builder.delta & Builder.PROXY) != 0 ? builder.proxy : builder.prototype.proxy;
105            authentication =
106                    (builder.delta & Builder.AUTH) != 0 ? builder.authentication : builder.prototype.authentication;
107            repositoryManager = (builder.delta & Builder.REPOMAN) != 0
108                    ? builder.repositoryManager
109                    : builder.prototype.repositoryManager;
110            blocked = (builder.delta & Builder.BLOCKED) != 0 ? builder.blocked : builder.prototype.blocked;
111            mirroredRepositories = (builder.delta & Builder.MIRRORED) != 0
112                    ? copy(builder.mirroredRepositories)
113                    : builder.prototype.mirroredRepositories;
114            intent = (builder.delta & Builder.INTENT) != 0 ? builder.intent : builder.prototype.intent;
115        } else {
116            id = builder.id;
117            type = builder.type;
118            url = builder.url;
119            releasePolicy = builder.releasePolicy;
120            snapshotPolicy = builder.snapshotPolicy;
121            proxy = builder.proxy;
122            authentication = builder.authentication;
123            repositoryManager = builder.repositoryManager;
124            blocked = builder.blocked;
125            mirroredRepositories = copy(builder.mirroredRepositories);
126            intent = builder.intent;
127        }
128
129        Matcher m = URL_PATTERN.matcher(url);
130        if (m.matches()) {
131            String h = m.group(5);
132            this.host = (h != null) ? h : "";
133            this.protocol = m.group(1);
134        } else {
135            this.host = "";
136            this.protocol = "";
137        }
138
139        this.hashCode = Objects.hash(
140                id,
141                type,
142                url, // host, protocol derived from url
143                releasePolicy,
144                snapshotPolicy,
145                proxy,
146                authentication,
147                mirroredRepositories,
148                repositoryManager,
149                blocked,
150                intent);
151    }
152
153    private static List<RemoteRepository> copy(List<RemoteRepository> repos) {
154        if (repos == null || repos.isEmpty()) {
155            return Collections.emptyList();
156        }
157        return Collections.unmodifiableList(Arrays.asList(repos.toArray(new RemoteRepository[0])));
158    }
159
160    @Override
161    public String getId() {
162        return id;
163    }
164
165    @Override
166    public String getContentType() {
167        return type;
168    }
169
170    /**
171     * Gets the (base) URL of this repository.
172     *
173     * @return The (base) URL of this repository, never {@code null}.
174     */
175    public String getUrl() {
176        return url;
177    }
178
179    /**
180     * Gets the protocol part from the repository's URL, for example {@code file} or {@code http}. As suggested by RFC
181     * 2396, section 3.1 "Scheme Component", the protocol name should be treated case-insensitively.
182     *
183     * @return The protocol or an empty string if none, never {@code null}.
184     */
185    public String getProtocol() {
186        return protocol;
187    }
188
189    /**
190     * Gets the host part from the repository's URL.
191     *
192     * @return The host or an empty string if none, never {@code null}.
193     */
194    public String getHost() {
195        return host;
196    }
197
198    /**
199     * Gets the policy to apply for snapshot/release artifacts.
200     *
201     * @param snapshot {@code true} to retrieve the snapshot policy, {@code false} to retrieve the release policy.
202     * @return The requested repository policy, never {@code null}.
203     */
204    public RepositoryPolicy getPolicy(boolean snapshot) {
205        return snapshot ? snapshotPolicy : releasePolicy;
206    }
207
208    /**
209     * Gets the proxy that has been selected for this repository.
210     *
211     * @return The selected proxy or {@code null} if none.
212     */
213    public Proxy getProxy() {
214        return proxy;
215    }
216
217    /**
218     * Gets the authentication that has been selected for this repository.
219     *
220     * @return The selected authentication or {@code null} if none.
221     */
222    public Authentication getAuthentication() {
223        return authentication;
224    }
225
226    /**
227     * Gets the repositories that this repository serves as a mirror for.
228     *
229     * @return The (read-only) repositories being mirrored by this repository, never {@code null}.
230     */
231    public List<RemoteRepository> getMirroredRepositories() {
232        return mirroredRepositories;
233    }
234
235    /**
236     * Indicates whether this repository refers to a repository manager or not.
237     *
238     * @return {@code true} if this repository is a repository manager, {@code false} otherwise.
239     */
240    public boolean isRepositoryManager() {
241        return repositoryManager;
242    }
243
244    /**
245     * Indicates whether this repository is blocked from performing any download requests.
246     *
247     * @return {@code true} if this repository is blocked from performing any download requests,
248     *         {@code false} otherwise.
249     */
250    public boolean isBlocked() {
251        return blocked;
252    }
253
254    /**
255     * Returns the intent this repository is prepared for.
256     *
257     * @return The intent this repository is prepared for.
258     * @since 2.0.14
259     */
260    public Intent getIntent() {
261        return intent;
262    }
263
264    @Override
265    public String toString() {
266        StringBuilder buffer = new StringBuilder(256);
267        buffer.append(getId());
268        buffer.append(" (").append(getUrl());
269        buffer.append(", ").append(getContentType());
270        boolean r = getPolicy(false).isEnabled(), s = getPolicy(true).isEnabled();
271        if (r && s) {
272            buffer.append(", releases+snapshots");
273        } else if (r) {
274            buffer.append(", releases");
275        } else if (s) {
276            buffer.append(", snapshots");
277        } else {
278            buffer.append(", disabled");
279        }
280        if (isRepositoryManager()) {
281            buffer.append(", managed");
282        }
283        if (isBlocked()) {
284            buffer.append(", blocked");
285        }
286        buffer.append(")");
287        return buffer.toString();
288    }
289
290    @Override
291    public boolean equals(Object obj) {
292        if (this == obj) {
293            return true;
294        }
295        if (obj == null || !getClass().equals(obj.getClass())) {
296            return false;
297        }
298
299        RemoteRepository that = (RemoteRepository) obj;
300
301        return Objects.equals(url, that.url)
302                && Objects.equals(type, that.type)
303                && Objects.equals(id, that.id)
304                && Objects.equals(releasePolicy, that.releasePolicy)
305                && Objects.equals(snapshotPolicy, that.snapshotPolicy)
306                && Objects.equals(proxy, that.proxy)
307                && Objects.equals(authentication, that.authentication)
308                && Objects.equals(mirroredRepositories, that.mirroredRepositories)
309                && repositoryManager == that.repositoryManager
310                && blocked == that.blocked
311                && intent == that.intent;
312    }
313
314    @Override
315    public int hashCode() {
316        return hashCode;
317    }
318
319    /**
320     * Makes "bare" repository out of this instance, usable as keys within one single session, by applying following
321     * changes to repository (returns new instance):
322     * <ul>
323     *     <li>sets intent to {@link Intent#BARE}</li>
324     *     <li>nullifies proxy</li>
325     *     <li>nullifies authentication</li>
326     *     <li>nullifies mirrors</li>
327     *     <li>sets repositoryManager to {@code false}</li>
328     * </ul>
329     * These properties are managed by repository system, based on configuration. See {@link org.eclipse.aether.RepositorySystem}
330     * and (internal component) {@code org.eclipse.aether.impl.RemoteRepositoryManager}.
331     *
332     * @return The "bare" repository, never {@code null}.
333     * @since 2.0.14
334     */
335    public RemoteRepository toBareRemoteRepository() {
336        return new Builder(this)
337                .setIntent(Intent.BARE)
338                .setProxy(null)
339                .setAuthentication(null)
340                .setMirroredRepositories(null)
341                .setRepositoryManager(false)
342                .build();
343    }
344
345    /**
346     * A builder to create remote repositories.
347     */
348    public static final class Builder {
349
350        private static final RepositoryPolicy DEFAULT_POLICY = new RepositoryPolicy();
351
352        static final int ID = 0x0001,
353                TYPE = 0x0002,
354                URL = 0x0004,
355                RELEASES = 0x0008,
356                SNAPSHOTS = 0x0010,
357                PROXY = 0x0020,
358                AUTH = 0x0040,
359                MIRRORED = 0x0080,
360                REPOMAN = 0x0100,
361                BLOCKED = 0x0200,
362                INTENT = 0x0400;
363
364        int delta;
365
366        RemoteRepository prototype;
367
368        String id;
369
370        String type;
371
372        String url;
373
374        RepositoryPolicy releasePolicy = DEFAULT_POLICY;
375
376        RepositoryPolicy snapshotPolicy = DEFAULT_POLICY;
377
378        Proxy proxy;
379
380        Authentication authentication;
381
382        List<RemoteRepository> mirroredRepositories;
383
384        boolean repositoryManager;
385
386        boolean blocked;
387
388        Intent intent = Intent.BARE;
389
390        /**
391         * Creates a new repository builder.
392         *
393         * @param id The identifier of the repository, may be {@code null}.
394         * @param type The type of the repository, may be {@code null}.
395         * @param url The (base) URL of the repository, may be {@code null}.
396         */
397        public Builder(String id, String type, String url) {
398            this.id = (id != null) ? id : "";
399            this.type = (type != null) ? type : "";
400            this.url = (url != null) ? url : "";
401        }
402
403        /**
404         * Creates a new repository builder which uses the specified remote repository as a prototype for the new one.
405         * All properties which have not been set on the builder will be copied from the prototype when building the
406         * repository.
407         *
408         * @param prototype The remote repository to use as prototype, must not be {@code null}.
409         */
410        public Builder(RemoteRepository prototype) {
411            this.prototype = requireNonNull(prototype, "remote repository prototype cannot be null");
412        }
413
414        /**
415         * Builds a new remote repository from the current values of this builder. The state of the builder itself
416         * remains unchanged.
417         *
418         * @return The remote repository, never {@code null}.
419         */
420        public RemoteRepository build() {
421            if (prototype != null && delta == 0) {
422                return prototype;
423            }
424            return new RemoteRepository(this);
425        }
426
427        private <T> void delta(int flag, T builder, T prototype) {
428            boolean equal = Objects.equals(builder, prototype);
429            if (equal) {
430                delta &= ~flag;
431            } else {
432                delta |= flag;
433            }
434        }
435
436        /**
437         * Sets the identifier of the repository.
438         *
439         * @param id The identifier of the repository, may be {@code null}.
440         * @return This builder for chaining, never {@code null}.
441         */
442        public Builder setId(String id) {
443            this.id = (id != null) ? id : "";
444            if (prototype != null) {
445                delta(ID, this.id, prototype.getId());
446            }
447            return this;
448        }
449
450        /**
451         * Sets the type of the repository, e.g. "default".
452         *
453         * @param type The type of the repository, may be {@code null}.
454         * @return This builder for chaining, never {@code null}.
455         */
456        public Builder setContentType(String type) {
457            this.type = (type != null) ? type : "";
458            if (prototype != null) {
459                delta(TYPE, this.type, prototype.getContentType());
460            }
461            return this;
462        }
463
464        /**
465         * Sets the (base) URL of the repository.
466         *
467         * @param url The URL of the repository, may be {@code null}.
468         * @return This builder for chaining, never {@code null}.
469         */
470        public Builder setUrl(String url) {
471            this.url = (url != null) ? url : "";
472            if (prototype != null) {
473                delta(URL, this.url, prototype.getUrl());
474            }
475            return this;
476        }
477
478        /**
479         * Sets the policy to apply for snapshot and release artifacts.
480         *
481         * @param policy The repository policy to set, may be {@code null} to use a default policy.
482         * @return This builder for chaining, never {@code null}.
483         */
484        public Builder setPolicy(RepositoryPolicy policy) {
485            this.releasePolicy = (policy != null) ? policy : DEFAULT_POLICY;
486            this.snapshotPolicy = (policy != null) ? policy : DEFAULT_POLICY;
487            if (prototype != null) {
488                delta(RELEASES, this.releasePolicy, prototype.getPolicy(false));
489                delta(SNAPSHOTS, this.snapshotPolicy, prototype.getPolicy(true));
490            }
491            return this;
492        }
493
494        /**
495         * Sets the policy to apply for release artifacts.
496         *
497         * @param releasePolicy The repository policy to set, may be {@code null} to use a default policy.
498         * @return This builder for chaining, never {@code null}.
499         */
500        public Builder setReleasePolicy(RepositoryPolicy releasePolicy) {
501            this.releasePolicy = (releasePolicy != null) ? releasePolicy : DEFAULT_POLICY;
502            if (prototype != null) {
503                delta(RELEASES, this.releasePolicy, prototype.getPolicy(false));
504            }
505            return this;
506        }
507
508        /**
509         * Sets the policy to apply for snapshot artifacts.
510         *
511         * @param snapshotPolicy The repository policy to set, may be {@code null} to use a default policy.
512         * @return This builder for chaining, never {@code null}.
513         */
514        public Builder setSnapshotPolicy(RepositoryPolicy snapshotPolicy) {
515            this.snapshotPolicy = (snapshotPolicy != null) ? snapshotPolicy : DEFAULT_POLICY;
516            if (prototype != null) {
517                delta(SNAPSHOTS, this.snapshotPolicy, prototype.getPolicy(true));
518            }
519            return this;
520        }
521
522        /**
523         * Sets the proxy to use in order to access the repository.
524         *
525         * @param proxy The proxy to use, may be {@code null}.
526         * @return This builder for chaining, never {@code null}.
527         */
528        public Builder setProxy(Proxy proxy) {
529            this.proxy = proxy;
530            if (prototype != null) {
531                delta(PROXY, this.proxy, prototype.getProxy());
532            }
533            return this;
534        }
535
536        /**
537         * Sets the authentication to use in order to access the repository.
538         *
539         * @param authentication The authentication to use, may be {@code null}.
540         * @return This builder for chaining, never {@code null}.
541         */
542        public Builder setAuthentication(Authentication authentication) {
543            this.authentication = authentication;
544            if (prototype != null) {
545                delta(AUTH, this.authentication, prototype.getAuthentication());
546            }
547            return this;
548        }
549
550        /**
551         * Sets the repositories being mirrored by the repository.
552         *
553         * @param mirroredRepositories The repositories being mirrored by the repository, may be {@code null}.
554         * @return This builder for chaining, never {@code null}.
555         */
556        public Builder setMirroredRepositories(List<RemoteRepository> mirroredRepositories) {
557            if (this.mirroredRepositories == null) {
558                this.mirroredRepositories = new ArrayList<>();
559            } else {
560                this.mirroredRepositories.clear();
561            }
562            if (mirroredRepositories != null) {
563                this.mirroredRepositories.addAll(mirroredRepositories);
564            }
565            if (prototype != null) {
566                delta(MIRRORED, this.mirroredRepositories, prototype.getMirroredRepositories());
567            }
568            return this;
569        }
570
571        /**
572         * Adds the specified repository to the list of repositories being mirrored by the repository. If this builder
573         * was {@link Builder constructed from a prototype}, the given repository
574         * will be added to the list of mirrored repositories from the prototype.
575         *
576         * @param mirroredRepository The repository being mirrored by the repository, may be {@code null}.
577         * @return This builder for chaining, never {@code null}.
578         */
579        public Builder addMirroredRepository(RemoteRepository mirroredRepository) {
580            if (mirroredRepository != null) {
581                if (this.mirroredRepositories == null) {
582                    this.mirroredRepositories = new ArrayList<>();
583                    if (prototype != null) {
584                        mirroredRepositories.addAll(prototype.getMirroredRepositories());
585                    }
586                }
587                mirroredRepositories.add(mirroredRepository);
588                if (prototype != null) {
589                    delta |= MIRRORED;
590                }
591            }
592            return this;
593        }
594
595        /**
596         * Marks the repository as a repository manager or not.
597         *
598         * @param repositoryManager {@code true} if the repository points at a repository manager, {@code false} if the
599         *            repository is just serving static contents.
600         * @return This builder for chaining, never {@code null}.
601         */
602        public Builder setRepositoryManager(boolean repositoryManager) {
603            this.repositoryManager = repositoryManager;
604            if (prototype != null) {
605                delta(REPOMAN, this.repositoryManager, prototype.isRepositoryManager());
606            }
607            return this;
608        }
609
610        /**
611         * Marks the repository as blocked or not.
612         *
613         * @param blocked {@code true} if the repository should not be allowed to perform any requests.
614         * @return This builder for chaining, never {@code null}.
615         */
616        public Builder setBlocked(boolean blocked) {
617            this.blocked = blocked;
618            if (prototype != null) {
619                delta(BLOCKED, this.blocked, prototype.isBlocked());
620            }
621            return this;
622        }
623
624        /**
625         * Marks the intent for this repository.
626         *
627         * @param intent the intent with this remote repository.
628         * @return This builder for chaining, never {@code null}.
629         * @since 2.0.14
630         */
631        public Builder setIntent(Intent intent) {
632            this.intent = intent;
633            if (prototype != null) {
634                delta(INTENT, this.intent, prototype.intent);
635            }
636            return this;
637        }
638    }
639}