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