1 /* 2 * Copyright 2015 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc; 18 19 import static com.google.common.base.Preconditions.checkArgument; 20 import static com.google.common.base.Preconditions.checkNotNull; 21 22 import com.google.common.base.Joiner; 23 import java.nio.charset.Charset; 24 import java.util.Collections; 25 import java.util.HashSet; 26 import java.util.LinkedHashMap; 27 import java.util.Map; 28 import java.util.Set; 29 import javax.annotation.Nullable; 30 import javax.annotation.concurrent.ThreadSafe; 31 32 /** 33 * Encloses classes related to the compression and decompression of messages. 34 */ 35 @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1704") 36 @ThreadSafe 37 public final class DecompressorRegistry { 38 static final Joiner ACCEPT_ENCODING_JOINER = Joiner.on(','); 39 emptyInstance()40 public static DecompressorRegistry emptyInstance() { 41 return new DecompressorRegistry(); 42 } 43 44 private static final DecompressorRegistry DEFAULT_INSTANCE = 45 emptyInstance() 46 .with(new Codec.Gzip(), true) 47 .with(Codec.Identity.NONE, false); 48 getDefaultInstance()49 public static DecompressorRegistry getDefaultInstance() { 50 return DEFAULT_INSTANCE; 51 } 52 53 private final Map<String, DecompressorInfo> decompressors; 54 private final byte[] advertisedDecompressors; 55 56 /** 57 * Registers a decompressor for both decompression and message encoding negotiation. Returns a 58 * new registry. 59 * 60 * @param d The decompressor to register 61 * @param advertised If true, the message encoding will be listed in the Accept-Encoding header. 62 */ with(Decompressor d, boolean advertised)63 public DecompressorRegistry with(Decompressor d, boolean advertised) { 64 return new DecompressorRegistry(d, advertised, this); 65 } 66 DecompressorRegistry(Decompressor d, boolean advertised, DecompressorRegistry parent)67 private DecompressorRegistry(Decompressor d, boolean advertised, DecompressorRegistry parent) { 68 String encoding = d.getMessageEncoding(); 69 checkArgument(!encoding.contains(","), "Comma is currently not allowed in message encoding"); 70 71 int newSize = parent.decompressors.size(); 72 if (!parent.decompressors.containsKey(d.getMessageEncoding())) { 73 newSize++; 74 } 75 Map<String, DecompressorInfo> newDecompressors = 76 new LinkedHashMap<>(newSize); 77 for (DecompressorInfo di : parent.decompressors.values()) { 78 String previousEncoding = di.decompressor.getMessageEncoding(); 79 if (!previousEncoding.equals(encoding)) { 80 newDecompressors.put( 81 previousEncoding, new DecompressorInfo(di.decompressor, di.advertised)); 82 } 83 } 84 newDecompressors.put(encoding, new DecompressorInfo(d, advertised)); 85 86 decompressors = Collections.unmodifiableMap(newDecompressors); 87 advertisedDecompressors = ACCEPT_ENCODING_JOINER.join(getAdvertisedMessageEncodings()) 88 .getBytes(Charset.forName("US-ASCII")); 89 } 90 DecompressorRegistry()91 private DecompressorRegistry() { 92 decompressors = new LinkedHashMap<>(0); 93 advertisedDecompressors = new byte[0]; 94 } 95 96 /** 97 * Provides a list of all message encodings that have decompressors available. 98 */ getKnownMessageEncodings()99 public Set<String> getKnownMessageEncodings() { 100 return decompressors.keySet(); 101 } 102 103 getRawAdvertisedMessageEncodings()104 byte[] getRawAdvertisedMessageEncodings() { 105 return advertisedDecompressors; 106 } 107 108 /** 109 * Provides a list of all message encodings that have decompressors available and should be 110 * advertised. 111 * 112 * <p>The specification doesn't say anything about ordering, or preference, so the returned codes 113 * can be arbitrary. 114 */ 115 @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1704") getAdvertisedMessageEncodings()116 public Set<String> getAdvertisedMessageEncodings() { 117 Set<String> advertisedDecompressors = new HashSet<>(decompressors.size()); 118 for (Map.Entry<String, DecompressorInfo> entry : decompressors.entrySet()) { 119 if (entry.getValue().advertised) { 120 advertisedDecompressors.add(entry.getKey()); 121 } 122 } 123 return Collections.unmodifiableSet(advertisedDecompressors); 124 } 125 126 /** 127 * Returns a decompressor for the given message encoding, or {@code null} if none has been 128 * registered. 129 * 130 * <p>This ignores whether the compressor is advertised. According to the spec, if we know how 131 * to process this encoding, we attempt to, regardless of whether or not it is part of the 132 * encodings sent to the remote host. 133 */ 134 @Nullable lookupDecompressor(String messageEncoding)135 public Decompressor lookupDecompressor(String messageEncoding) { 136 DecompressorInfo info = decompressors.get(messageEncoding); 137 return info != null ? info.decompressor : null; 138 } 139 140 /** 141 * Information about a decompressor. 142 */ 143 private static final class DecompressorInfo { 144 final Decompressor decompressor; 145 final boolean advertised; 146 DecompressorInfo(Decompressor decompressor, boolean advertised)147 DecompressorInfo(Decompressor decompressor, boolean advertised) { 148 this.decompressor = checkNotNull(decompressor, "decompressor"); 149 this.advertised = advertised; 150 } 151 } 152 } 153