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}