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")
+ );
+ }
+ }
+}