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.Map.Entry; 29 import java.util.Set; 30 import javax.annotation.Nullable; 31 import javax.annotation.concurrent.ThreadSafe; 32 33 /** 34 * Encloses classes related to the compression and decompression of messages. 35 */ 36 @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1704") 37 @ThreadSafe 38 public final class DecompressorRegistry { 39 static final Joiner ACCEPT_ENCODING_JOINER = Joiner.on(','); 40 emptyInstance()41 public static DecompressorRegistry emptyInstance() { 42 return new DecompressorRegistry(); 43 } 44 45 private static final DecompressorRegistry DEFAULT_INSTANCE = 46 emptyInstance() 47 .with(new Codec.Gzip(), true) 48 .with(Codec.Identity.NONE, false); 49 getDefaultInstance()50 public static DecompressorRegistry getDefaultInstance() { 51 return DEFAULT_INSTANCE; 52 } 53 54 private final Map<String, DecompressorInfo> decompressors; 55 private final byte[] advertisedDecompressors; 56 57 /** 58 * Registers a decompressor for both decompression and message encoding negotiation. Returns a 59 * new registry. 60 * 61 * @param d The decompressor to register 62 * @param advertised If true, the message encoding will be listed in the Accept-Encoding header. 63 */ with(Decompressor d, boolean advertised)64 public DecompressorRegistry with(Decompressor d, boolean advertised) { 65 return new DecompressorRegistry(d, advertised, this); 66 } 67 DecompressorRegistry(Decompressor d, boolean advertised, DecompressorRegistry parent)68 private DecompressorRegistry(Decompressor d, boolean advertised, DecompressorRegistry parent) { 69 String encoding = d.getMessageEncoding(); 70 checkArgument(!encoding.contains(","), "Comma is currently not allowed in message encoding"); 71 72 int newSize = parent.decompressors.size(); 73 if (!parent.decompressors.containsKey(d.getMessageEncoding())) { 74 newSize++; 75 } 76 Map<String, DecompressorInfo> newDecompressors = 77 new LinkedHashMap<String, DecompressorInfo>(newSize); 78 for (DecompressorInfo di : parent.decompressors.values()) { 79 String previousEncoding = di.decompressor.getMessageEncoding(); 80 if (!previousEncoding.equals(encoding)) { 81 newDecompressors.put( 82 previousEncoding, new DecompressorInfo(di.decompressor, di.advertised)); 83 } 84 } 85 newDecompressors.put(encoding, new DecompressorInfo(d, advertised)); 86 87 decompressors = Collections.unmodifiableMap(newDecompressors); 88 advertisedDecompressors = ACCEPT_ENCODING_JOINER.join(getAdvertisedMessageEncodings()) 89 .getBytes(Charset.forName("US-ASCII")); 90 } 91 DecompressorRegistry()92 private DecompressorRegistry() { 93 decompressors = new LinkedHashMap<String, DecompressorInfo>(0); 94 advertisedDecompressors = new byte[0]; 95 } 96 97 /** 98 * Provides a list of all message encodings that have decompressors available. 99 */ getKnownMessageEncodings()100 public Set<String> getKnownMessageEncodings() { 101 return decompressors.keySet(); 102 } 103 104 getRawAdvertisedMessageEncodings()105 byte[] getRawAdvertisedMessageEncodings() { 106 return advertisedDecompressors; 107 } 108 109 /** 110 * Provides a list of all message encodings that have decompressors available and should be 111 * advertised. 112 * 113 * <p>The specification doesn't say anything about ordering, or preference, so the returned codes 114 * can be arbitrary. 115 */ 116 @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1704") getAdvertisedMessageEncodings()117 public Set<String> getAdvertisedMessageEncodings() { 118 Set<String> advertisedDecompressors = new HashSet<String>(decompressors.size()); 119 for (Entry<String, DecompressorInfo> entry : decompressors.entrySet()) { 120 if (entry.getValue().advertised) { 121 advertisedDecompressors.add(entry.getKey()); 122 } 123 } 124 return Collections.unmodifiableSet(advertisedDecompressors); 125 } 126 127 /** 128 * Returns a decompressor for the given message encoding, or {@code null} if none has been 129 * registered. 130 * 131 * <p>This ignores whether the compressor is advertised. According to the spec, if we know how 132 * to process this encoding, we attempt to, regardless of whether or not it is part of the 133 * encodings sent to the remote host. 134 */ 135 @Nullable lookupDecompressor(String messageEncoding)136 public Decompressor lookupDecompressor(String messageEncoding) { 137 DecompressorInfo info = decompressors.get(messageEncoding); 138 return info != null ? info.decompressor : null; 139 } 140 141 /** 142 * Information about a decompressor. 143 */ 144 private static final class DecompressorInfo { 145 final Decompressor decompressor; 146 final boolean advertised; 147 DecompressorInfo(Decompressor decompressor, boolean advertised)148 DecompressorInfo(Decompressor decompressor, boolean advertised) { 149 this.decompressor = checkNotNull(decompressor, "decompressor"); 150 this.advertised = advertised; 151 } 152 } 153 } 154