99import java .net .http .HttpRequest ;
1010import java .net .http .HttpResponse ;
1111import java .util .List ;
12- import java .util .Objects ;
1312import java .util .Optional ;
1413import java .util .Set ;
1514import java .util .concurrent .CompletableFuture ;
@@ -65,7 +64,7 @@ public static List<String> extractLinks(String content, Set<LinkFilter> filter)
6564 * @return true if the content contains at least one link
6665 */
6766 public static boolean containsLink (String content ) {
68- return !( new UrlDetector (content , UrlDetectorOptions .BRACKET_MATCH ).detect ().isEmpty () );
67+ return !new UrlDetector (content , UrlDetectorOptions .BRACKET_MATCH ).detect ().isEmpty ();
6968 }
7069
7170 /**
@@ -74,39 +73,46 @@ public static boolean containsLink(String content) {
7473 * <p>
7574 * A link is considered broken if:
7675 * <ul>
77- * <li>The URL is invalid or malformed</li>
7876 * <li>An HTTP request fails</li>
7977 * <li>The HTTP response status code is outside the 200–399 range</li>
8078 * </ul>
8179 *
8280 * <p>
81+ * The method first performs an HTTP {@code HEAD} request and falls back to an HTTP {@code GET}
82+ * request if the {@code HEAD} request indicates a failure.
83+ * </p>
84+ *
85+ * <p>
8386 * Notes:
8487 * <ul>
8588 * <li>Status code {@code 200} is considered valid, even if the response body is empty</li>
8689 * <li>The response body content is not inspected</li>
8790 * </ul>
8891 *
89- * @param url the URL to check
92+ * @param url the URL to check (must be a valid {@link URI})
9093 * @return a future completing with {@code true} if the link is broken
94+ * @throws IllegalArgumentException if the given URL is not a valid URI
9195 */
9296
9397 public static CompletableFuture <Boolean > isLinkBroken (String url ) {
94- HttpRequest headRequest = HttpRequest .newBuilder (URI .create (url ))
98+ HttpRequest headCheckRequest = HttpRequest .newBuilder (URI .create (url ))
9599 .method ("HEAD" , HttpRequest .BodyPublishers .noBody ())
96100 .build ();
97101
98- return HTTP_CLIENT .sendAsync (headRequest , HttpResponse .BodyHandlers .discarding ())
102+ return HTTP_CLIENT .sendAsync (headCheckRequest , HttpResponse .BodyHandlers .discarding ())
99103 .thenApply (response -> {
100104 int status = response .statusCode ();
101105 return status < 200 || status >= 400 ;
102106 })
103107 .exceptionally (ignored -> true )
104108 .thenCompose (result -> {
105- if (!Boolean . TRUE . equals ( result ) ) {
109+ if (!result ) {
106110 return CompletableFuture .completedFuture (false );
107111 }
108- HttpRequest getRequest = HttpRequest .newBuilder (URI .create (url )).GET ().build ();
109- return HTTP_CLIENT .sendAsync (getRequest , HttpResponse .BodyHandlers .discarding ())
112+ HttpRequest getFallbackRequest =
113+ HttpRequest .newBuilder (URI .create (url )).GET ().build ();
114+ return HTTP_CLIENT
115+ .sendAsync (getFallbackRequest , HttpResponse .BodyHandlers .discarding ())
110116 .thenApply (resp -> resp .statusCode () >= 400 )
111117 .exceptionally (ignored -> true ); // still never null
112118 });
@@ -116,6 +122,10 @@ public static CompletableFuture<Boolean> isLinkBroken(String url) {
116122 * Replaces all broken links in the given text with the provided replacement string.
117123 *
118124 * <p>
125+ * The link checks are performed asynchronously.
126+ * </p>
127+ *
128+ * <p>
119129 * Example:
120130 *
121131 * <pre>{@code
@@ -135,29 +145,28 @@ public static CompletableFuture<Boolean> isLinkBroken(String url) {
135145 * http://workinglink/1
136146 * }</pre>
137147 *
138- * @param text the input text containing URLs
139- * @param replacement the string to replace broken links with
148+ * @param text the input text containing URLs (must not be {@code null})
149+ * @param replacement the string to replace broken links with (must not be {@code null})
140150 * @return a future containing the modified text
141151 */
142-
143152 public static CompletableFuture <String > replaceDeadLinks (String text , String replacement ) {
144153 List <String > links = extractLinks (text , DEFAULT_FILTERS );
145154
146155 if (links .isEmpty ()) {
147156 return CompletableFuture .completedFuture (text );
148157 }
149158
150- List <CompletableFuture <String >> deadLinkFutures = links .stream ()
159+ List <CompletableFuture <Optional < String > >> deadLinkFutures = links .stream ()
151160 .distinct ()
152161 .map (link -> isLinkBroken (link )
153- .thenApply (isBroken -> Boolean .TRUE .equals (isBroken ) ? link : null ))
154-
162+ .thenApply (isBroken -> isBroken ? Optional .of (link ) : Optional .<String >empty ()))
155163 .toList ();
156164
157- return CompletableFuture .allOf (deadLinkFutures .toArray (new CompletableFuture [0 ]))
165+
166+ return CompletableFuture .allOf (deadLinkFutures .toArray (CompletableFuture []::new ))
158167 .thenApply (ignored -> deadLinkFutures .stream ()
159168 .map (CompletableFuture ::join )
160- .filter ( Objects :: nonNull )
169+ .flatMap ( Optional :: stream )
161170 .toList ())
162171 .thenApply (deadLinks -> {
163172 String result = text ;
0 commit comments