1 | /*- | |
2 | * #%L | |
3 | * io.earcam.utilitarian.site.deploy.netlify | |
4 | * %% | |
5 | * Copyright (C) 2017 earcam | |
6 | * %% | |
7 | * SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT) | |
8 | * | |
9 | * You <b>must</b> choose to accept, in full - any individual or combination of | |
10 | * the following licenses: | |
11 | * <ul> | |
12 | * <li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li> | |
13 | * <li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li> | |
14 | * <li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li> | |
15 | * <li><a href="https://opensource.org/licenses/MIT">MIT</a></li> | |
16 | * </ul> | |
17 | * #L% | |
18 | */ | |
19 | package io.earcam.utilitarian.site.deploy.netlify; | |
20 | ||
21 | import static io.earcam.unexceptional.Closing.closeAfterAccepting; | |
22 | import static java.time.Instant.MIN; | |
23 | import static java.util.Collections.singletonMap; | |
24 | import static javax.ws.rs.client.ClientBuilder.newBuilder; | |
25 | import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; | |
26 | import static javax.ws.rs.core.Response.Status.Family.SUCCESSFUL; | |
27 | ||
28 | import java.io.IOException; | |
29 | import java.io.OutputStream; | |
30 | import java.net.URI; | |
31 | import java.nio.file.Files; | |
32 | import java.nio.file.Path; | |
33 | import java.nio.file.attribute.FileTime; | |
34 | import java.util.List; | |
35 | import java.util.Map; | |
36 | import java.util.Map.Entry; | |
37 | import java.util.Optional; | |
38 | import java.util.stream.Collectors; | |
39 | import java.util.zip.ZipEntry; | |
40 | import java.util.zip.ZipOutputStream; | |
41 | ||
42 | import javax.annotation.WillClose; | |
43 | import javax.json.JsonArray; | |
44 | import javax.json.JsonObject; | |
45 | import javax.json.JsonValue; | |
46 | import javax.ws.rs.client.Client; | |
47 | import javax.ws.rs.client.Entity; | |
48 | import javax.ws.rs.core.MediaType; | |
49 | import javax.ws.rs.core.Response; | |
50 | import javax.ws.rs.core.StreamingOutput; | |
51 | ||
52 | import org.slf4j.Logger; | |
53 | import org.slf4j.LoggerFactory; | |
54 | ||
55 | import io.earcam.unexceptional.CheckedConsumer; | |
56 | import io.earcam.unexceptional.EmeticStream; | |
57 | import io.earcam.utilitarian.web.jaxrs.JsonMessageBodyReader; | |
58 | import io.earcam.utilitarian.web.jaxrs.JsonMessageBodyWriter; | |
59 | import io.earcam.utilitarian.web.jaxrs.TokenBearerAuthenticator; | |
60 | import io.earcam.utilitarian.web.jaxrs.UserAgent; | |
61 | ||
62 | /** | |
63 | * API client for <a href="https://netlify.com">Netlify</a> | |
64 | */ | |
65 | public class Netlify { | |
66 | ||
67 | private static final String USER_AGENT = "Mozilla/5.0 (X11; YouNix; Linux x86_64; rv:53.0) earcam.io/1.0"; | |
68 | ||
69 | private static final MediaType APPLICATION_ZIP_TYPE = new MediaType("application", "zip"); | |
70 | ||
71 | public static final String BASE_URL = "https://api.netlify.com/api/v1/"; | |
72 | ||
73 | private static final Logger LOG = LoggerFactory.getLogger(Netlify.class); | |
74 | ||
75 | private Client client; | |
76 | private String baseUrl; | |
77 | ||
78 | ||
79 | public Netlify(String accessToken) | |
80 | { | |
81 | this(accessToken, newBuilder().build(), BASE_URL); | |
82 | } | |
83 | ||
84 | ||
85 | public Netlify(String accessToken, String baseUrl) | |
86 | { | |
87 | this(accessToken, newBuilder().build(), baseUrl); | |
88 | } | |
89 | ||
90 | ||
91 | public Netlify(String accessToken, Client client, String baseUrl) | |
92 | { | |
93 | this.client = configure(accessToken, client); | |
94 | this.baseUrl = ensureTrailingSlash(baseUrl); | |
95 | } | |
96 | ||
97 | ||
98 | private static Client configure(String accessToken, Client client) | |
99 | { | |
100 |
1
1. configure : mutated return of Object value for io/earcam/utilitarian/site/deploy/netlify/Netlify::configure to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return client.register(new TokenBearerAuthenticator(accessToken)) |
101 | .register(new UserAgent(USER_AGENT)) | |
102 | .register(new JsonMessageBodyReader()) | |
103 | .register(new JsonMessageBodyWriter()); | |
104 | } | |
105 | ||
106 | ||
107 | private String ensureTrailingSlash(String earl) | |
108 | { | |
109 |
3
1. ensureTrailingSlash : Replaced integer subtraction with addition → KILLED 2. ensureTrailingSlash : negated conditional → KILLED 3. ensureTrailingSlash : mutated return of Object value for io/earcam/utilitarian/site/deploy/netlify/Netlify::ensureTrailingSlash to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return earl.charAt(earl.length() - 1) == '/' ? earl : earl + '/'; |
110 | } | |
111 | ||
112 | ||
113 | public Site create(Site site) | |
114 | { | |
115 | StreamingOutput o = site::writeJson; | |
116 | ||
117 | Response response = client.target(baseUrl + "sites") | |
118 | .request(APPLICATION_JSON_TYPE) | |
119 | .post(Entity.entity(o, APPLICATION_JSON_TYPE)); | |
120 | ||
121 |
1
1. create : removed call to io/earcam/utilitarian/site/deploy/netlify/Netlify::checkSuccessful → SURVIVED |
checkSuccessful(response); |
122 | ||
123 | JsonObject json = response.readEntity(JsonObject.class); | |
124 |
1
1. create : mutated return of Object value for io/earcam/utilitarian/site/deploy/netlify/Netlify::create to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return Site.fromJsonObject(json); |
125 | } | |
126 | ||
127 | ||
128 | static void checkSuccessful(Response response) | |
129 | { | |
130 |
1
1. checkSuccessful : negated conditional → KILLED |
if(response.getStatusInfo().getFamily() != SUCCESSFUL) { // should be a ??? |
131 | throw requestFailedException(response); | |
132 | } | |
133 | } | |
134 | ||
135 | ||
136 | static IllegalStateException requestFailedException(Response response) | |
137 | { | |
138 |
1
1. requestFailedException : mutated return of Object value for io/earcam/utilitarian/site/deploy/netlify/Netlify::requestFailedException to ( if (x != null) null else throw new RuntimeException ) → NO_COVERAGE |
return new IllegalStateException(response.getStatus() + " - " + |
139 | response.getStatusInfo().getReasonPhrase()); | |
140 | } | |
141 | ||
142 | ||
143 | public List<Site> list() | |
144 | { | |
145 | throw new UnsupportedOperationException("TODO"); | |
146 | } | |
147 | ||
148 | ||
149 | public void destroy(String siteName) | |
150 | { | |
151 | Site site = siteForName(siteName); | |
152 | ||
153 | Response response = client.target(baseUrl + "sites/" + site.id()) | |
154 | .request() | |
155 | .delete(); | |
156 | ||
157 |
1
1. destroy : removed call to io/earcam/utilitarian/site/deploy/netlify/Netlify::checkSuccessful → SURVIVED |
checkSuccessful(response); |
158 | } | |
159 | ||
160 | ||
161 | private Site siteForName(String siteName) | |
162 | { | |
163 |
2
1. lambda$siteForName$0 : mutated return of Object value for io/earcam/utilitarian/site/deploy/netlify/Netlify::lambda$siteForName$0 to ( if (x != null) null else throw new RuntimeException ) → SURVIVED 2. siteForName : mutated return of Object value for io/earcam/utilitarian/site/deploy/netlify/Netlify::siteForName to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return findSiteForName(siteName).orElseThrow(() -> new RuntimeException("No site found with name: " + siteName)); |
164 | } | |
165 | ||
166 | ||
167 | public void deployZip(String siteName, String uploadPath, Path baseDir) | |
168 | { | |
169 |
1
1. deployZip : removed call to io/earcam/utilitarian/site/deploy/netlify/Netlify::deployZip → SURVIVED |
deployZip(siteName, singletonMap(uploadPath, baseDir)); |
170 | } | |
171 | ||
172 | ||
173 | public void deployZip(String siteName, Map<String, Path> baseDirs) | |
174 | { | |
175 | Site site = siteForName(siteName); | |
176 |
1
1. deployZip : removed call to io/earcam/utilitarian/site/deploy/netlify/Netlify::deploySiteZip → SURVIVED |
deploySiteZip(baseDirs, site); |
177 | } | |
178 | ||
179 | ||
180 | protected void deploySiteZip(Map<String, Path> baseDirs, Site site) | |
181 | { | |
182 |
1
1. lambda$deploySiteZip$1 : removed call to io/earcam/utilitarian/site/deploy/netlify/Netlify::writeZip → KILLED |
StreamingOutput body = o -> writeZip(baseDirs, o); |
183 | ||
184 | Response response = client.target(baseUrl + "sites/" + site.id() + "/deploys") | |
185 | .request(APPLICATION_JSON_TYPE) | |
186 | .post(Entity.entity(body, APPLICATION_ZIP_TYPE)); | |
187 | ||
188 | LOG.debug("response: {}", response); | |
189 |
1
1. deploySiteZip : removed call to io/earcam/utilitarian/site/deploy/netlify/Netlify::checkSuccessful → SURVIVED |
checkSuccessful(response); |
190 | ||
191 | JsonObject json = response.readEntity(JsonObject.class); | |
192 | ||
193 | String state = json.getString("state", "unknown"); | |
194 |
1
1. deploySiteZip : negated conditional → KILLED |
if(!"uploaded".equals(state)) { |
195 | throw new IllegalStateException("Response JSON doesn't include 'state=\"uploaded\"', " + state + ". JSON: " + json); | |
196 | } | |
197 | } | |
198 | ||
199 | ||
200 | protected void writeZip(Map<String, Path> baseDirs, @WillClose OutputStream output) | |
201 | { | |
202 |
1
1. writeZip : removed call to io/earcam/unexceptional/Closing::closeAfterAccepting → KILLED |
closeAfterAccepting(ZipOutputStream::new, output, baseDirs, this::doWriteZip); |
203 | } | |
204 | ||
205 | ||
206 | private void doWriteZip(ZipOutputStream zip, Map<String, Path> baseDirs) | |
207 | { | |
208 | @SuppressWarnings("squid:S1905") // false positive; cast IS required | |
209 |
1
1. lambda$doWriteZip$2 : removed call to java/util/zip/ZipOutputStream::closeEntry → SURVIVED |
CheckedConsumer<byte[], IOException> writeThenClose = ((CheckedConsumer<byte[], IOException>) zip::write).andThen(b -> zip.closeEntry()); |
210 | ||
211 | for(Entry<String, Path> e : baseDirs.entrySet()) { | |
212 |
1
1. lambda$doWriteZip$3 : mutated return of Object value for io/earcam/utilitarian/site/deploy/netlify/Netlify::lambda$doWriteZip$3 to ( if (x != null) null else throw new RuntimeException ) → KILLED |
EmeticStream.emesis(Files::walk, e.getValue()) |
213 | .sequential() | |
214 | .sorted(Path::compareTo) | |
215 |
1
1. lambda$doWriteZip$4 : replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED |
.filter(Files::isRegularFile) |
216 | // Quick hack, TODO add excludes, but sitemap should also clean up it's cache files | |
217 |
2
1. lambda$doWriteZip$5 : negated conditional → KILLED 2. lambda$doWriteZip$5 : replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED |
.filter(p -> !p.getFileName().toString().startsWith(".io.earcam.utilitarian.site.sitemap.")) |
218 | .peek(f -> LOG.debug("Writing to zip: {}", f)) | |
219 |
1
1. lambda$doWriteZip$7 : removed call to io/earcam/utilitarian/site/deploy/netlify/Netlify::zipEntry → KILLED |
.peek(f -> zipEntry(zip, e, f)) |
220 | .map(Files::readAllBytes) | |
221 |
1
1. doWriteZip : removed call to io/earcam/unexceptional/EmeticStream::forEach → KILLED |
.forEach(writeThenClose); |
222 | } | |
223 | } | |
224 | ||
225 | ||
226 | /* | |
227 | * Timestamps set to constant values purely to make wire-mock testing easy (irrelevant for Netlify) | |
228 | */ | |
229 | private void zipEntry(ZipOutputStream zip, Entry<String, Path> baseDir, Path file) throws IOException | |
230 | { | |
231 | URI relativePath = baseDir.getValue().toUri().relativize(file.toUri()); | |
232 | String absolutePath = baseDir.getKey() + relativePath; | |
233 | ZipEntry entry = new ZipEntry(absolutePath); | |
234 | LOG.debug("Absolute path in site: /{}", absolutePath); | |
235 |
1
1. zipEntry : removed call to java/util/zip/ZipEntry::setTime → SURVIVED |
entry.setTime(0); |
236 | entry.setCreationTime(FileTime.from(MIN)); | |
237 | entry.setLastAccessTime(FileTime.from(MIN)); | |
238 | entry.setLastModifiedTime(FileTime.from(MIN)); | |
239 |
1
1. zipEntry : removed call to java/util/zip/ZipOutputStream::putNextEntry → KILLED |
zip.putNextEntry(entry); |
240 | } | |
241 | ||
242 | ||
243 | public Optional<Site> findSiteForName(String siteName) | |
244 | { | |
245 | List<Site> json = siteList(); | |
246 | ||
247 |
1
1. findSiteForName : mutated return of Object value for io/earcam/utilitarian/site/deploy/netlify/Netlify::findSiteForName to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return json.stream() |
248 |
1
1. lambda$findSiteForName$8 : replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED |
.filter(s -> siteName.equals(s.name())) |
249 | .findAny(); | |
250 | } | |
251 | ||
252 | ||
253 | List<Site> siteList() | |
254 | { | |
255 |
1
1. siteList : mutated return of Object value for io/earcam/utilitarian/site/deploy/netlify/Netlify::siteList to ( if (x != null) null else throw new RuntimeException ) → KILLED |
return client.target(baseUrl + "sites") |
256 | .request(APPLICATION_JSON_TYPE) | |
257 | .get() | |
258 | .readEntity(JsonArray.class) | |
259 | .stream() | |
260 | .map(JsonValue::asJsonObject) | |
261 | .map(Site::fromJsonObject) | |
262 | .collect(Collectors.toList()); | |
263 | } | |
264 | } | |
Mutations | ||
100 |
1.1 |
|
109 |
1.1 2.2 3.3 |
|
121 |
1.1 |
|
124 |
1.1 |
|
130 |
1.1 |
|
138 |
1.1 |
|
157 |
1.1 |
|
163 |
1.1 2.2 |
|
169 |
1.1 |
|
176 |
1.1 |
|
182 |
1.1 |
|
189 |
1.1 |
|
194 |
1.1 |
|
202 |
1.1 |
|
209 |
1.1 |
|
212 |
1.1 |
|
215 |
1.1 |
|
217 |
1.1 2.2 |
|
219 |
1.1 |
|
221 |
1.1 |
|
235 |
1.1 |
|
239 |
1.1 |
|
247 |
1.1 |
|
248 |
1.1 |
|
255 |
1.1 |