001/* 002 * Copyright 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet; 018 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.ThreadSafe; 023import java.util.Objects; 024import java.util.Optional; 025 026import static java.lang.String.format; 027import static java.util.Objects.requireNonNull; 028 029/** 030 * Immutable representation of one HTTP entity tag. 031 * 032 * @author <a href="https://www.revetkn.com">Mark Allen</a> 033 */ 034@ThreadSafe 035public final class EntityTag { 036 @NonNull 037 private final String value; 038 @NonNull 039 private final Boolean weak; 040 041 @NonNull 042 public static EntityTag fromStrongValue(@NonNull String value) { 043 requireNonNull(value); 044 return new EntityTag(value, false); 045 } 046 047 @NonNull 048 public static EntityTag fromWeakValue(@NonNull String value) { 049 requireNonNull(value); 050 return new EntityTag(value, true); 051 } 052 053 @NonNull 054 public static Optional<EntityTag> fromHeaderValue(@Nullable String headerValue) { 055 String trimmed = Utilities.trimAggressivelyToNull(headerValue); 056 057 if (trimmed == null || "*".equals(trimmed)) 058 return Optional.empty(); 059 060 Boolean weak = false; 061 String remaining = trimmed; 062 063 if (remaining.regionMatches(true, 0, "W/", 0, 2)) { 064 weak = true; 065 remaining = remaining.substring(2); 066 } 067 068 if (remaining.length() < 2 || remaining.charAt(0) != '"' || remaining.charAt(remaining.length() - 1) != '"') 069 return Optional.empty(); 070 071 String value = remaining.substring(1, remaining.length() - 1); 072 073 if (!isValidOpaqueTag(value)) 074 return Optional.empty(); 075 076 return Optional.of(new EntityTag(value, weak)); 077 } 078 079 private EntityTag(@NonNull String value, 080 @NonNull Boolean weak) { 081 requireNonNull(value); 082 requireNonNull(weak); 083 084 if (!isValidOpaqueTag(value)) 085 throw new IllegalArgumentException(format("Invalid entity-tag value '%s'.", value)); 086 087 this.value = value; 088 this.weak = weak; 089 } 090 091 @NonNull 092 public Boolean isWeak() { 093 return this.weak; 094 } 095 096 @NonNull 097 public String getValue() { 098 return this.value; 099 } 100 101 @NonNull 102 public String toHeaderValue() { 103 return format("%s\"%s\"", isWeak() ? "W/" : "", getValue()); 104 } 105 106 @NonNull 107 public Boolean stronglyMatches(@NonNull EntityTag other) { 108 requireNonNull(other); 109 return !isWeak() && !other.isWeak() && getValue().equals(other.getValue()); 110 } 111 112 @NonNull 113 public Boolean weaklyMatches(@NonNull EntityTag other) { 114 requireNonNull(other); 115 return getValue().equals(other.getValue()); 116 } 117 118 @Override 119 public String toString() { 120 return format("%s{value=%s, weak=%s}", getClass().getSimpleName(), getValue(), isWeak()); 121 } 122 123 @Override 124 public boolean equals(@Nullable Object object) { 125 if (this == object) 126 return true; 127 128 if (!(object instanceof EntityTag entityTag)) 129 return false; 130 131 return Objects.equals(getValue(), entityTag.getValue()) 132 && Objects.equals(isWeak(), entityTag.isWeak()); 133 } 134 135 @Override 136 public int hashCode() { 137 return Objects.hash(getValue(), isWeak()); 138 } 139 140 private static boolean isValidOpaqueTag(@NonNull String value) { 141 requireNonNull(value); 142 143 for (int i = 0; i < value.length(); i++) { 144 char c = value.charAt(i); 145 146 if (c == '"' || c < 0x21 || c == 0x7F) 147 return false; 148 } 149 150 return true; 151 } 152}