001package ball.net; 002/*- 003 * ########################################################################## 004 * Utilities 005 * $Id: ResponseCacheImpl.java 5855 2020-04-27 20:01:17Z ball $ 006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-util/trunk/src/main/java/ball/net/ResponseCacheImpl.java $ 007 * %% 008 * Copyright (C) 2008 - 2020 Allen D. Ball 009 * %% 010 * Licensed under the Apache License, Version 2.0 (the "License"); 011 * you may not use this file except in compliance with the License. 012 * You may obtain a copy of the License at 013 * 014 * http://www.apache.org/licenses/LICENSE-2.0 015 * 016 * Unless required by applicable law or agreed to in writing, software 017 * distributed under the License is distributed on an "AS IS" BASIS, 018 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 019 * See the License for the specific language governing permissions and 020 * limitations under the License. 021 * ########################################################################## 022 */ 023import java.beans.XMLDecoder; 024import java.beans.XMLEncoder; 025import java.io.IOException; 026import java.io.InputStream; 027import java.io.OutputStream; 028import java.net.CacheRequest; 029import java.net.CacheResponse; 030import java.net.ResponseCache; 031import java.net.URI; 032import java.net.URLConnection; 033import java.nio.file.Files; 034import java.nio.file.Path; 035import java.nio.file.Paths; 036import java.nio.file.attribute.PosixFilePermission; 037import java.util.List; 038import java.util.Map; 039import java.util.Objects; 040import java.util.Set; 041import lombok.AllArgsConstructor; 042import lombok.NoArgsConstructor; 043import lombok.NonNull; 044import lombok.ToString; 045 046import static java.nio.file.attribute.PosixFilePermissions.asFileAttribute; 047import static java.nio.file.attribute.PosixFilePermissions.fromString; 048import static lombok.AccessLevel.PRIVATE; 049import static org.apache.commons.lang3.StringUtils.isNotEmpty; 050 051/** 052 * {@link ResponseCache} implementation. 053 * 054 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball} 055 * @version $Revision: 5855 $ 056 */ 057@NoArgsConstructor(access = PRIVATE) @ToString 058public class ResponseCacheImpl extends ResponseCache { 059 060 /** 061 * Default {@link ResponseCacheImpl}. 062 */ 063 public static final ResponseCacheImpl DEFAULT = new ResponseCacheImpl(); 064 065 private static final String BODY = "BODY"; 066 private static final String HEADERS = "HEADERS"; 067 068 private final Path cache = 069 Paths.get(System.getProperty("user.home"), ".config", "java", "cache"); 070 071 { 072 try { 073 Files.createDirectories(cache, 074 asFileAttribute(fromString("rwx------"))); 075 } catch (Exception exception) { 076 throw new ExceptionInInitializerError(exception); 077 } 078 } 079 080 @Override 081 public CacheResponse get(URI uri, String method, 082 Map<String,List<String>> headers) { 083 CacheResponseImpl response = null; 084 085 if (isCached(uri)) { 086 response = new CacheResponseImpl(cache(uri)); 087 } 088 089 return response; 090 } 091 092 @Override 093 public CacheRequest put(URI uri, URLConnection connection) { 094 CacheRequestImpl request = null; 095 096 if (isCacheable(uri)) { 097 if (! connection.getAllowUserInteraction()) { 098 request = 099 new CacheRequestImpl(cache(uri), 100 connection.getHeaderFields()); 101 } 102 } 103 104 return request; 105 } 106 107 private Path cache(URI uri) { 108 Path path = cache.resolve(uri.getScheme().toLowerCase()); 109 String host = uri.getHost().toLowerCase(); 110 int port = uri.getPort(); 111 112 if (port > 0) { 113 host += ":" + String.valueOf(port); 114 } 115 116 path = path.resolve(host); 117 118 String string = uri.getPath(); 119 120 if (string != null) { 121 for (String substring : string.split("[/]+")) { 122 if (isNotEmpty(substring)) { 123 path = path.resolve(substring); 124 } 125 } 126 } 127 128 return path.normalize(); 129 } 130 131 private boolean isCached(URI uri) { 132 return isCacheable(uri) && Files.exists(cache(uri).resolve(BODY)); 133 } 134 135 private boolean isCacheable(URI uri) { 136 return (uri.isAbsolute() 137 && (! uri.isOpaque()) 138 && uri.getUserInfo() == null 139 && uri.getQuery() == null 140 && uri.getFragment() == null); 141 } 142 143 private void delete(Path path) throws IOException { 144 Files.deleteIfExists(path.resolve(HEADERS)); 145 Files.deleteIfExists(path.resolve(BODY)); 146 Files.deleteIfExists(path); 147 } 148 149 @AllArgsConstructor(access = PRIVATE) @ToString 150 public class CacheRequestImpl extends CacheRequest { 151 @NonNull private final Path path; 152 @NonNull private final Map<String,List<String>> headers; 153 154 @Override 155 public OutputStream getBody() throws IOException { 156 Files.createDirectories(path); 157 158 XMLEncoder encoder = 159 new XMLEncoder(Files.newOutputStream(path.resolve(HEADERS))); 160 161 encoder.writeObject(headers); 162 encoder.close(); 163 164 return Files.newOutputStream(path.resolve(BODY)); 165 } 166 167 @Override 168 public void abort() { 169 try { 170 delete(path); 171 } catch (Exception exception) { 172 throw new IllegalStateException(exception); 173 } 174 } 175 } 176 177 @AllArgsConstructor(access = PRIVATE) @ToString 178 public class CacheResponseImpl extends CacheResponse { 179 @NonNull private final Path path; 180 181 @Override 182 public Map<String,List<String>> getHeaders() throws IOException { 183 XMLDecoder decoder = 184 new XMLDecoder(Files.newInputStream(path.resolve(HEADERS))); 185 @SuppressWarnings("unchecked") 186 Map<String,List<String>> headers = 187 (Map<String,List<String>>) decoder.readObject(); 188 189 decoder.close(); 190 191 return headers; 192 } 193 194 @Override 195 public InputStream getBody() throws IOException { 196 return Files.newInputStream(path.resolve(BODY)); 197 } 198 } 199}