diff --git a/pom.xml b/pom.xml index 0e908df6f..9f372d703 100644 --- a/pom.xml +++ b/pom.xml @@ -105,6 +105,12 @@ 4.11 test + + org.hamcrest + hamcrest-all + 1.3 + test + com.fasterxml.jackson.core jackson-databind diff --git a/src/main/java/org/kohsuke/github/GHObject.java b/src/main/java/org/kohsuke/github/GHObject.java index a81d913a5..3a1cde190 100644 --- a/src/main/java/org/kohsuke/github/GHObject.java +++ b/src/main/java/org/kohsuke/github/GHObject.java @@ -3,14 +3,15 @@ package org.kohsuke.github; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.apache.commons.lang.builder.ReflectionToStringBuilder; -import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.lang.builder.ToStringStyle; -import org.apache.commons.lang.reflect.FieldUtils; +import javax.annotation.CheckForNull; import java.io.IOException; import java.lang.reflect.Field; import java.net.URL; import java.util.Date; +import java.util.List; +import java.util.Map; /** * Most (all?) domain objects in GitHub seems to have these 4 properties. @@ -18,6 +19,9 @@ import java.util.Date; @SuppressFBWarnings(value = {"UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD"}, justification = "JSON API") public abstract class GHObject { + // not data but information related to data from responce + protected Map> responseHeaderFields; + protected String url; protected int id; protected String created_at; @@ -26,6 +30,11 @@ public abstract class GHObject { /*package*/ GHObject() { } + @CheckForNull + public Map> getResponseHeaderFields() { + return responseHeaderFields; + } + /** * When was this resource created? */ diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index db70fbd64..64aefe68f 100644 --- a/src/main/java/org/kohsuke/github/Requester.java +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -25,6 +25,10 @@ package org.kohsuke.github; import com.fasterxml.jackson.databind.JsonMappingException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.apache.commons.io.IOUtils; +import org.kohsuke.github.exception.GHFileNotFoundException; +import org.kohsuke.github.exception.GHIOException; + import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -48,17 +52,18 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; -import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; + +import javax.annotation.CheckForNull; import javax.annotation.WillClose; -import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import static java.util.Arrays.asList; -import static java.util.logging.Level.*; +import static java.util.logging.Level.FINE; +import static java.util.logging.Level.FINEST; import static org.kohsuke.github.GitHub.MAPPER; /** @@ -269,7 +274,7 @@ class Requester { if (nextLinkMatcher.find()) { final String link = nextLinkMatcher.group(1); T nextResult = _to(link, type, instance); - + injectInResult(nextResult); final int resultLength = Array.getLength(result); final int nextResultLength = Array.getLength(nextResult); T concatResult = (T) Array.newInstance(type.getComponentType(), resultLength + nextResultLength); @@ -279,6 +284,7 @@ class Requester { } } } + injectInResult(result); return result; } catch (IOException e) { handleApiError(e); @@ -579,6 +585,7 @@ class Requester { throw new IllegalStateException("Failed to set the request method to "+method); } + @CheckForNull private T parse(Class type, T instance) throws IOException { InputStreamReader r = null; int responseCode = -1; @@ -598,12 +605,17 @@ class Requester { String data = IOUtils.toString(r); if (type!=null) try { - return MAPPER.readValue(data,type); + final T readValue = MAPPER.readValue(data, type); + injectInResult(readValue); + return readValue; } catch (JsonMappingException e) { throw (IOException)new IOException("Failed to deserialize " +data).initCause(e); } - if (instance!=null) - return MAPPER.readerForUpdating(instance).readValue(data); + if (instance!=null) { + final T readValue = MAPPER.readerForUpdating(instance).readValue(data); + injectInResult(readValue); + return readValue; + } return null; } catch (FileNotFoundException e) { // java.net.URLConnection handles 404 exception has FileNotFoundException, don't wrap exception in HttpException @@ -616,6 +628,26 @@ class Requester { } } + private void injectInResult(T readValue) { + if (readValue instanceof GHObject[]) { + for (GHObject ghObject : (GHObject[]) readValue) { + injectInResult(ghObject); + } + } else if (readValue instanceof GHObject) { + injectInResult((GHObject) readValue); + } + } + + private void injectInResult(GHObject readValue) { + try { + final Field field = GHObject.class.getDeclaredField("responseHeaderFields"); + field.setAccessible(true); + field.set(readValue, uc.getHeaderFields()); + } catch (NoSuchFieldException ignore) { + } catch (IllegalAccessException ignore) { + } + } + /** * Handles the "Content-Encoding" header. */ @@ -663,15 +695,16 @@ class Requester { String error = IOUtils.toString(es, "UTF-8"); if (e instanceof FileNotFoundException) { // pass through 404 Not Found to allow the caller to handle it intelligently - throw (IOException) new FileNotFoundException(error).initCause(e); + throw (IOException) new GHFileNotFoundException(error).withResponseHeaderFields(uc).initCause(e); } else if (e instanceof HttpException) { HttpException http = (HttpException) e; throw (IOException) new HttpException(error, http.getResponseCode(), http.getResponseMessage(), http.getUrl(), e); } else { - throw (IOException) new IOException(error).initCause(e); + throw (IOException) new GHIOException(error).withResponceHeaderFields(uc).initCause(e); } - } else + } else { throw e; + } } finally { IOUtils.closeQuietly(es); } diff --git a/src/main/java/org/kohsuke/github/exception/GHFileNotFoundException.java b/src/main/java/org/kohsuke/github/exception/GHFileNotFoundException.java new file mode 100644 index 000000000..27606de59 --- /dev/null +++ b/src/main/java/org/kohsuke/github/exception/GHFileNotFoundException.java @@ -0,0 +1,34 @@ +package org.kohsuke.github.exception; + +import javax.annotation.CheckForNull; +import java.io.FileNotFoundException; +import java.net.HttpURLConnection; +import java.util.List; +import java.util.Map; + +/** + * Request/responce contains useful metadata. + * Custom exception allows store info for next diagnostics. + * + * @author Kanstantsin Shautsou + */ +public class GHFileNotFoundException extends FileNotFoundException { + protected Map> responseHeaderFields; + + public GHFileNotFoundException() { + } + + public GHFileNotFoundException(String s) { + super(s); + } + + @CheckForNull + public Map> getResponseHeaderFields() { + return responseHeaderFields; + } + + public GHFileNotFoundException withResponseHeaderFields(HttpURLConnection urlConnection) { + this.responseHeaderFields = urlConnection.getHeaderFields(); + return this; + } +} diff --git a/src/main/java/org/kohsuke/github/exception/GHIOException.java b/src/main/java/org/kohsuke/github/exception/GHIOException.java new file mode 100644 index 000000000..7bb614092 --- /dev/null +++ b/src/main/java/org/kohsuke/github/exception/GHIOException.java @@ -0,0 +1,42 @@ +package org.kohsuke.github.exception; + +import javax.annotation.CheckForNull; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.List; +import java.util.Map; + +/** + * Request/responce contains useful metadata. + * Custom exception allows store info for next diagnostics. + * + * @author Kanstantsin Shautsou + */ +public class GHIOException extends IOException { + protected Map> responceHeaderFields; + + public GHIOException() { + } + + public GHIOException(String message) { + super(message); + } + + public GHIOException(String message, Throwable cause) { + super(message, cause); + } + + public GHIOException(Throwable cause) { + super(cause); + } + + @CheckForNull + public Map> getResponceHeaderFields() { + return responceHeaderFields; + } + + public GHIOException withResponceHeaderFields(HttpURLConnection urlConnection) { + this.responceHeaderFields = urlConnection.getHeaderFields(); + return this; + } +} diff --git a/src/test/java/org/kohsuke/github/GHHookTest.java b/src/test/java/org/kohsuke/github/GHHookTest.java new file mode 100644 index 000000000..2cbab48cb --- /dev/null +++ b/src/test/java/org/kohsuke/github/GHHookTest.java @@ -0,0 +1,79 @@ +package org.kohsuke.github; + +import org.apache.commons.lang.StringUtils; +import org.junit.Ignore; +import org.junit.Test; +import org.kohsuke.github.exception.GHFileNotFoundException; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.hasValue; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.Assert.assertThat; + + +/** + * @author Kanstantsin Shautsou + */ +public class GHHookTest { + + @Ignore + @Test + public void exposeResponceHeaders() throws Exception { + String user1Login = "KostyaSha-auto"; + String user1Pass = "secret"; + + String clientId = "90140219451"; + String clientSecret = "1451245425"; + + String orgRepo = "KostyaSha-org/test"; + + // some login based user that has access to application + final GitHub gitHub = GitHub.connectUsingPassword(user1Login, user1Pass); + gitHub.getMyself(); + + // we request read + final List scopes = Arrays.asList("repo", "read:org", "user:email", "read:repo_hook"); + + // application creates token with scopes + final GHAuthorization auth = gitHub.createOrGetAuth(clientId, clientSecret, scopes, "", ""); + String token = auth.getToken(); + if (StringUtils.isEmpty(token)) { + gitHub.deleteAuth(auth.getId()); + token = gitHub.createOrGetAuth(clientId, clientSecret, scopes, "", "").getToken(); + } + + /// now create connection using token + final GitHub gitHub2 = GitHub.connectUsingOAuth(token); + // some repo in organisation + final GHRepository repository = gitHub2.getRepository(orgRepo); + + // doesn't fail because we have read access + final List hooks = repository.getHooks(); + + try { + // fails because application isn't approved in organisation and you can find it only after doing real call + final GHHook hook = repository.createHook( + "my-hook", + singletonMap("url", "http://localhost"), + singletonList(GHEvent.PUSH), + true + ); + } catch (IOException ex) { + assertThat(ex, instanceOf(GHFileNotFoundException.class)); + final GHFileNotFoundException ghFileNotFoundException = (GHFileNotFoundException) ex; + final Map> responseHeaderFields = ghFileNotFoundException.getResponseHeaderFields(); + assertThat(responseHeaderFields, hasKey("X-Accepted-OAuth-Scopes")); + assertThat(responseHeaderFields.get("X-Accepted-OAuth-Scopes"), + hasItem("admin:repo_hook, public_repo, repo, write:repo_hook") + ); + } + } +}