Netlify.java

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
Location : configure
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.listSites
mutated return of Object value for io/earcam/utilitarian/site/deploy/netlify/Netlify::configure to ( if (x != null) null else throw new RuntimeException ) → KILLED

109

1.1
Location : ensureTrailingSlash
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.listSites
Replaced integer subtraction with addition → KILLED

2.2
Location : ensureTrailingSlash
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.listSites
negated conditional → KILLED

3.3
Location : ensureTrailingSlash
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.listSites
mutated return of Object value for io/earcam/utilitarian/site/deploy/netlify/Netlify::ensureTrailingSlash to ( if (x != null) null else throw new RuntimeException ) → KILLED

121

1.1
Location : create
Killed by : none
removed call to io/earcam/utilitarian/site/deploy/netlify/Netlify::checkSuccessful → SURVIVED

124

1.1
Location : create
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.createSite
mutated return of Object value for io/earcam/utilitarian/site/deploy/netlify/Netlify::create to ( if (x != null) null else throw new RuntimeException ) → KILLED

130

1.1
Location : checkSuccessful
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.listDeploys
negated conditional → KILLED

138

1.1
Location : requestFailedException
Killed by : none
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

157

1.1
Location : destroy
Killed by : none
removed call to io/earcam/utilitarian/site/deploy/netlify/Netlify::checkSuccessful → SURVIVED

163

1.1
Location : lambda$siteForName$0
Killed by : none
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.2
Location : siteForName
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.destroySite
mutated return of Object value for io/earcam/utilitarian/site/deploy/netlify/Netlify::siteForName to ( if (x != null) null else throw new RuntimeException ) → KILLED

169

1.1
Location : deployZip
Killed by : none
removed call to io/earcam/utilitarian/site/deploy/netlify/Netlify::deployZip → SURVIVED

176

1.1
Location : deployZip
Killed by : none
removed call to io/earcam/utilitarian/site/deploy/netlify/Netlify::deploySiteZip → SURVIVED

182

1.1
Location : lambda$deploySiteZip$1
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.deployZip
removed call to io/earcam/utilitarian/site/deploy/netlify/Netlify::writeZip → KILLED

189

1.1
Location : deploySiteZip
Killed by : none
removed call to io/earcam/utilitarian/site/deploy/netlify/Netlify::checkSuccessful → SURVIVED

194

1.1
Location : deploySiteZip
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.deployZip
negated conditional → KILLED

202

1.1
Location : writeZip
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.deployZip
removed call to io/earcam/unexceptional/Closing::closeAfterAccepting → KILLED

209

1.1
Location : lambda$doWriteZip$2
Killed by : none
removed call to java/util/zip/ZipOutputStream::closeEntry → SURVIVED

212

1.1
Location : lambda$doWriteZip$3
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.deployZip
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

215

1.1
Location : lambda$doWriteZip$4
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.deployZip
replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED

217

1.1
Location : lambda$doWriteZip$5
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.deployZip
negated conditional → KILLED

2.2
Location : lambda$doWriteZip$5
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.deployZip
replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED

219

1.1
Location : lambda$doWriteZip$7
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.deployZip
removed call to io/earcam/utilitarian/site/deploy/netlify/Netlify::zipEntry → KILLED

221

1.1
Location : doWriteZip
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.deployZip
removed call to io/earcam/unexceptional/EmeticStream::forEach → KILLED

235

1.1
Location : zipEntry
Killed by : none
removed call to java/util/zip/ZipEntry::setTime → SURVIVED

239

1.1
Location : zipEntry
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.deployZip
removed call to java/util/zip/ZipOutputStream::putNextEntry → KILLED

247

1.1
Location : findSiteForName
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.findSiteId
mutated return of Object value for io/earcam/utilitarian/site/deploy/netlify/Netlify::findSiteForName to ( if (x != null) null else throw new RuntimeException ) → KILLED

248

1.1
Location : lambda$findSiteForName$8
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.findSiteId
replaced return of integer sized value with (x == 0 ? 1 : 0) → KILLED

255

1.1
Location : siteList
Killed by : io.earcam.utilitarian.site.deploy.netlify.NetlifyTest.listSites
mutated return of Object value for io/earcam/utilitarian/site/deploy/netlify/Netlify::siteList to ( if (x != null) null else throw new RuntimeException ) → KILLED

Active mutators

Tests examined


Report generated by PIT 1.4.3