FloLei
1 year ago
committed by
GitHub
18 changed files with 1429 additions and 0 deletions
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<!-- |
||||
|
||||
Copyright 2012-2023 The Feign Authors |
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
in compliance with the License. You may obtain a copy of the License at |
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
or implied. See the License for the specific language governing permissions and limitations under |
||||
the License. |
||||
|
||||
--> |
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
|
||||
<parent> |
||||
<groupId>io.github.openfeign</groupId> |
||||
<artifactId>parent</artifactId> |
||||
<version>12.4-SNAPSHOT</version> |
||||
</parent> |
||||
|
||||
<artifactId>feign-jaxb-jakarta</artifactId> |
||||
<name>Feign JAXB Jakarta</name> |
||||
<description>Feign JAXB Jakarta</description> |
||||
|
||||
<properties> |
||||
<main.basedir>${project.basedir}/..</main.basedir> |
||||
</properties> |
||||
|
||||
<dependencies> |
||||
<dependency> |
||||
<groupId>${project.groupId}</groupId> |
||||
<artifactId>feign-core</artifactId> |
||||
</dependency> |
||||
|
||||
<dependency> |
||||
<groupId>${project.groupId}</groupId> |
||||
<artifactId>feign-core</artifactId> |
||||
<type>test-jar</type> |
||||
<scope>test</scope> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>jakarta.xml.bind</groupId> |
||||
<artifactId>jakarta.xml.bind-api</artifactId> |
||||
<version>4.0.0</version> |
||||
</dependency> |
||||
|
||||
<!-- Test --> |
||||
<dependency> |
||||
<groupId>com.sun.xml.bind</groupId> |
||||
<artifactId>jaxb-impl</artifactId> |
||||
<version>4.0.0</version> |
||||
<scope>test</scope> |
||||
</dependency> |
||||
</dependencies> |
||||
|
||||
</project> |
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
package feign.jaxb; |
||||
|
||||
/** |
||||
* Encapsulate data used to build the cache key of JAXBContext. |
||||
*/ |
||||
interface JAXBContextCacheKey { |
||||
} |
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
package feign.jaxb; |
||||
|
||||
import java.util.Objects; |
||||
|
||||
/** |
||||
* Encapsulate data used to build the cache key of JAXBContext when created using class mode. |
||||
*/ |
||||
final class JAXBContextClassCacheKey implements JAXBContextCacheKey { |
||||
|
||||
private final Class<?> clazz; |
||||
|
||||
JAXBContextClassCacheKey(Class<?> clazz) { |
||||
this.clazz = clazz; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object o) { |
||||
if (this == o) |
||||
return true; |
||||
if (o == null || getClass() != o.getClass()) |
||||
return false; |
||||
JAXBContextClassCacheKey that = (JAXBContextClassCacheKey) o; |
||||
return clazz.equals(that.clazz); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return Objects.hash(clazz); |
||||
} |
||||
} |
@ -0,0 +1,196 @@
@@ -0,0 +1,196 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
package feign.jaxb; |
||||
|
||||
import jakarta.xml.bind.*; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
import java.util.concurrent.ConcurrentHashMap; |
||||
|
||||
/** |
||||
* Creates and caches JAXB contexts as well as creates Marshallers and Unmarshallers for each |
||||
* context. Since JAXB contexts creation can be an expensive task, JAXB context can be preloaded on |
||||
* factory creation otherwise they will be created and cached dynamically when needed. |
||||
*/ |
||||
public final class JAXBContextFactory { |
||||
|
||||
private final ConcurrentHashMap<JAXBContextCacheKey, JAXBContext> jaxbContexts = |
||||
new ConcurrentHashMap<>(64); |
||||
private final Map<String, Object> properties; |
||||
private final JAXBContextInstantationMode jaxbContextInstantationMode; |
||||
|
||||
private JAXBContextFactory(Map<String, Object> properties, |
||||
JAXBContextInstantationMode jaxbContextInstantationMode) { |
||||
this.properties = properties; |
||||
this.jaxbContextInstantationMode = jaxbContextInstantationMode; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link jakarta.xml.bind.Unmarshaller} that handles the supplied class. |
||||
*/ |
||||
public Unmarshaller createUnmarshaller(Class<?> clazz) throws JAXBException { |
||||
return getContext(clazz).createUnmarshaller(); |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link jakarta.xml.bind.Marshaller} that handles the supplied class. |
||||
*/ |
||||
public Marshaller createMarshaller(Class<?> clazz) throws JAXBException { |
||||
Marshaller marshaller = getContext(clazz).createMarshaller(); |
||||
setMarshallerProperties(marshaller); |
||||
return marshaller; |
||||
} |
||||
|
||||
private void setMarshallerProperties(Marshaller marshaller) throws PropertyException { |
||||
for (Entry<String, Object> en : properties.entrySet()) { |
||||
marshaller.setProperty(en.getKey(), en.getValue()); |
||||
} |
||||
} |
||||
|
||||
private JAXBContext getContext(Class<?> clazz) throws JAXBException { |
||||
JAXBContextCacheKey cacheKey = jaxbContextInstantationMode.getJAXBContextCacheKey(clazz); |
||||
JAXBContext jaxbContext = this.jaxbContexts.get(cacheKey); |
||||
|
||||
if (jaxbContext == null) { |
||||
jaxbContext = jaxbContextInstantationMode.getJAXBContext(clazz); |
||||
this.jaxbContexts.putIfAbsent(cacheKey, jaxbContext); |
||||
} |
||||
return jaxbContext; |
||||
} |
||||
|
||||
/** |
||||
* Will preload factory's cache with JAXBContext for provided classes |
||||
* |
||||
* @param classes |
||||
* @throws jakarta.xml.bind.JAXBException |
||||
*/ |
||||
private void preloadContextCache(List<Class<?>> classes) throws JAXBException { |
||||
if (classes != null && !classes.isEmpty()) { |
||||
for (Class<?> clazz : classes) { |
||||
getContext(clazz); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Creates instances of {@link JAXBContextFactory}. |
||||
*/ |
||||
public static class Builder { |
||||
|
||||
private final Map<String, Object> properties = new HashMap<>(10); |
||||
|
||||
private JAXBContextInstantationMode jaxbContextInstantationMode = |
||||
JAXBContextInstantationMode.CLASS; |
||||
|
||||
/** |
||||
* Sets the jaxb.encoding property of any Marshaller created by this factory. |
||||
*/ |
||||
public Builder withMarshallerJAXBEncoding(String value) { |
||||
properties.put(Marshaller.JAXB_ENCODING, value); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the jaxb.schemaLocation property of any Marshaller created by this factory. |
||||
*/ |
||||
public Builder withMarshallerSchemaLocation(String value) { |
||||
properties.put(Marshaller.JAXB_SCHEMA_LOCATION, value); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the jaxb.noNamespaceSchemaLocation property of any Marshaller created by this factory. |
||||
*/ |
||||
public Builder withMarshallerNoNamespaceSchemaLocation(String value) { |
||||
properties.put(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION, value); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the jaxb.formatted.output property of any Marshaller created by this factory. |
||||
*/ |
||||
public Builder withMarshallerFormattedOutput(Boolean value) { |
||||
properties.put(Marshaller.JAXB_FORMATTED_OUTPUT, value); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the jaxb.fragment property of any Marshaller created by this factory. |
||||
*/ |
||||
public Builder withMarshallerFragment(Boolean value) { |
||||
properties.put(Marshaller.JAXB_FRAGMENT, value); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the given property of any Marshaller created by this factory. |
||||
* |
||||
* <p> |
||||
* Example : <br> |
||||
* <br> |
||||
* <code> |
||||
* new JAXBContextFactory.Builder() |
||||
* .withProperty("com.sun.xml.internal.bind.xmlHeaders", "<!DOCTYPE Example SYSTEM \"example.dtd\">") |
||||
* .build(); |
||||
* </code> |
||||
* </p> |
||||
*/ |
||||
public Builder withProperty(String key, Object value) { |
||||
properties.put(key, value); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Provide an instantiation mode for JAXB Contexts, can be class or package, default is class if |
||||
* this method is not called. |
||||
* |
||||
* <p> |
||||
* Example : <br> |
||||
* <br> |
||||
* <code> |
||||
* new JAXBContextFactory.Builder() |
||||
* .withJAXBContextInstantiationMode(JAXBContextInstantationMode.PACKAGE) |
||||
* .build(); |
||||
* </code> |
||||
* </p> |
||||
*/ |
||||
public Builder withJAXBContextInstantiationMode(JAXBContextInstantationMode jaxbContextInstantiationMode) { |
||||
this.jaxbContextInstantationMode = jaxbContextInstantiationMode; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link JAXBContextFactory} instance with a lazy loading cached context |
||||
*/ |
||||
public JAXBContextFactory build() { |
||||
return new JAXBContextFactory(properties, jaxbContextInstantationMode); |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link JAXBContextFactory} instance. Pre-loads context cache with given classes |
||||
* |
||||
* @param classes |
||||
* @return ContextFactory with a pre-populated JAXBContext cache |
||||
* @throws jakarta.xml.bind.JAXBException if provided classes can't be used for JAXBContext |
||||
* generation most likely due to missing JAXB annotations |
||||
*/ |
||||
public JAXBContextFactory build(List<Class<?>> classes) throws JAXBException { |
||||
JAXBContextFactory factory = new JAXBContextFactory(properties, jaxbContextInstantationMode); |
||||
factory.preloadContextCache(classes); |
||||
return factory; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
package feign.jaxb; |
||||
|
||||
|
||||
import jakarta.xml.bind.JAXBContext; |
||||
import jakarta.xml.bind.JAXBException; |
||||
|
||||
/** |
||||
* Provides differents ways to instantiate a JAXB Context. |
||||
*/ |
||||
public enum JAXBContextInstantationMode { |
||||
|
||||
CLASS { |
||||
@Override |
||||
JAXBContextCacheKey getJAXBContextCacheKey(Class<?> clazz) { |
||||
return new JAXBContextClassCacheKey(clazz); |
||||
} |
||||
|
||||
@Override |
||||
JAXBContext getJAXBContext(Class<?> clazz) throws JAXBException { |
||||
return JAXBContext.newInstance(clazz); |
||||
} |
||||
}, |
||||
|
||||
PACKAGE { |
||||
@Override |
||||
JAXBContextCacheKey getJAXBContextCacheKey(Class<?> clazz) { |
||||
return new JAXBContextPackageCacheKey(clazz.getPackage().getName(), clazz.getClassLoader()); |
||||
} |
||||
|
||||
@Override |
||||
JAXBContext getJAXBContext(Class<?> clazz) throws JAXBException { |
||||
return JAXBContext.newInstance(clazz.getPackage().getName(), clazz.getClassLoader()); |
||||
} |
||||
}; |
||||
|
||||
abstract JAXBContextCacheKey getJAXBContextCacheKey(Class<?> clazz); |
||||
|
||||
abstract JAXBContext getJAXBContext(Class<?> clazz) throws JAXBException; |
||||
} |
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
package feign.jaxb; |
||||
|
||||
import java.util.Objects; |
||||
|
||||
/** |
||||
* Encapsulate data used to build the cache key of JAXBContext when created using package mode. |
||||
*/ |
||||
final class JAXBContextPackageCacheKey implements JAXBContextCacheKey { |
||||
|
||||
private final String packageName; |
||||
|
||||
private final ClassLoader classLoader; |
||||
|
||||
JAXBContextPackageCacheKey(String packageName, ClassLoader classLoader) { |
||||
this.packageName = packageName; |
||||
this.classLoader = classLoader; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object o) { |
||||
if (this == o) |
||||
return true; |
||||
if (o == null || getClass() != o.getClass()) |
||||
return false; |
||||
JAXBContextPackageCacheKey that = (JAXBContextPackageCacheKey) o; |
||||
return packageName.equals(that.packageName) && classLoader.equals(that.classLoader); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return Objects.hash(packageName, classLoader); |
||||
} |
||||
} |
@ -0,0 +1,127 @@
@@ -0,0 +1,127 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
package feign.jaxb; |
||||
|
||||
import feign.Response; |
||||
import feign.Util; |
||||
import feign.codec.DecodeException; |
||||
import feign.codec.Decoder; |
||||
import jakarta.xml.bind.JAXBException; |
||||
import org.xml.sax.InputSource; |
||||
import org.xml.sax.SAXException; |
||||
import javax.xml.parsers.ParserConfigurationException; |
||||
import javax.xml.parsers.SAXParserFactory; |
||||
import javax.xml.transform.sax.SAXSource; |
||||
import java.io.IOException; |
||||
import java.lang.reflect.ParameterizedType; |
||||
import java.lang.reflect.Type; |
||||
|
||||
/** |
||||
* Decodes responses using JAXB with Jakarta Api. <br> |
||||
* <p> |
||||
* Basic example with with Feign.Builder: |
||||
* </p> |
||||
* |
||||
* <pre> |
||||
* JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder() |
||||
* .withMarshallerJAXBEncoding("UTF-8") |
||||
* .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
|
||||
* .build(); |
||||
* |
||||
* api = Feign.builder() |
||||
* .decoder(new JAXBDecoder(jaxbFactory)) |
||||
* .target(MyApi.class, "http://api");
|
||||
* </pre> |
||||
* <p> |
||||
* The JAXBContextFactory should be reused across requests as it caches the created JAXB contexts. |
||||
* </p> |
||||
*/ |
||||
public class JAXBDecoder implements Decoder { |
||||
|
||||
private final JAXBContextFactory jaxbContextFactory; |
||||
private final boolean namespaceAware; |
||||
|
||||
public JAXBDecoder(JAXBContextFactory jaxbContextFactory) { |
||||
this.jaxbContextFactory = jaxbContextFactory; |
||||
this.namespaceAware = true; |
||||
} |
||||
|
||||
private JAXBDecoder(Builder builder) { |
||||
this.jaxbContextFactory = builder.jaxbContextFactory; |
||||
this.namespaceAware = builder.namespaceAware; |
||||
} |
||||
|
||||
@Override |
||||
public Object decode(Response response, Type type) throws IOException { |
||||
if (response.status() == 404 || response.status() == 204) |
||||
return Util.emptyValueOf(type); |
||||
if (response.body() == null) |
||||
return null; |
||||
while (type instanceof ParameterizedType) { |
||||
ParameterizedType ptype = (ParameterizedType) type; |
||||
type = ptype.getRawType(); |
||||
} |
||||
if (!(type instanceof Class)) { |
||||
throw new UnsupportedOperationException( |
||||
"JAXB only supports decoding raw types. Found " + type); |
||||
} |
||||
|
||||
|
||||
try { |
||||
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); |
||||
/* Explicitly control sax configuration to prevent XXE attacks */ |
||||
saxParserFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); |
||||
saxParserFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); |
||||
saxParserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false); |
||||
saxParserFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", |
||||
false); |
||||
saxParserFactory.setNamespaceAware(namespaceAware); |
||||
|
||||
return jaxbContextFactory.createUnmarshaller((Class<?>) type).unmarshal(new SAXSource( |
||||
saxParserFactory.newSAXParser().getXMLReader(), |
||||
new InputSource(response.body().asInputStream()))); |
||||
} catch (JAXBException | ParserConfigurationException | SAXException e) { |
||||
throw new DecodeException(response.status(), e.toString(), response.request(), e); |
||||
} finally { |
||||
if (response.body() != null) { |
||||
response.body().close(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public static class Builder { |
||||
private boolean namespaceAware = true; |
||||
private JAXBContextFactory jaxbContextFactory; |
||||
|
||||
/** |
||||
* Controls whether the underlying XML parser is namespace aware. Default is true. |
||||
*/ |
||||
public Builder withNamespaceAware(boolean namespaceAware) { |
||||
this.namespaceAware = namespaceAware; |
||||
return this; |
||||
} |
||||
|
||||
public Builder withJAXBContextFactory(JAXBContextFactory jaxbContextFactory) { |
||||
this.jaxbContextFactory = jaxbContextFactory; |
||||
return this; |
||||
} |
||||
|
||||
public JAXBDecoder build() { |
||||
if (jaxbContextFactory == null) { |
||||
throw new IllegalStateException("JAXBContextFactory must be non-null"); |
||||
} |
||||
return new JAXBDecoder(this); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,67 @@
@@ -0,0 +1,67 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
package feign.jaxb; |
||||
|
||||
import feign.RequestTemplate; |
||||
import feign.codec.EncodeException; |
||||
import feign.codec.Encoder; |
||||
import jakarta.xml.bind.JAXBException; |
||||
import jakarta.xml.bind.Marshaller; |
||||
import java.io.StringWriter; |
||||
import java.lang.reflect.Type; |
||||
|
||||
/** |
||||
* Encodes requests using JAXB with Jakarta Api. <br> |
||||
* <p> |
||||
* Basic example with with Feign.Builder: |
||||
* </p> |
||||
* |
||||
* <pre> |
||||
* JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder() |
||||
* .withMarshallerJAXBEncoding("UTF-8") |
||||
* .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd") |
||||
* .build(); |
||||
* |
||||
* api = Feign.builder() |
||||
* .encoder(new JAXBEncoder(jaxbFactory)) |
||||
* .target(MyApi.class, "http://api"); |
||||
* </pre> |
||||
* <p> |
||||
* The JAXBContextFactory should be reused across requests as it caches the created JAXB contexts. |
||||
* </p> |
||||
*/ |
||||
public class JAXBEncoder implements Encoder { |
||||
|
||||
private final JAXBContextFactory jaxbContextFactory; |
||||
|
||||
public JAXBEncoder(JAXBContextFactory jaxbContextFactory) { |
||||
this.jaxbContextFactory = jaxbContextFactory; |
||||
} |
||||
|
||||
@Override |
||||
public void encode(Object object, Type bodyType, RequestTemplate template) { |
||||
if (!(bodyType instanceof Class)) { |
||||
throw new UnsupportedOperationException( |
||||
"JAXB only supports encoding raw types. Found " + bodyType); |
||||
} |
||||
try { |
||||
Marshaller marshaller = jaxbContextFactory.createMarshaller((Class<?>) bodyType); |
||||
StringWriter stringWriter = new StringWriter(); |
||||
marshaller.marshal(object, stringWriter); |
||||
template.body(stringWriter.toString()); |
||||
} catch (JAXBException e) { |
||||
throw new EncodeException(e.toString(), e); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,280 @@
@@ -0,0 +1,280 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
package feign.jaxb; |
||||
|
||||
import feign.Request; |
||||
import feign.Request.HttpMethod; |
||||
import feign.RequestTemplate; |
||||
import feign.Response; |
||||
import feign.Util; |
||||
import feign.codec.Encoder; |
||||
import jakarta.xml.bind.annotation.XmlAccessType; |
||||
import jakarta.xml.bind.annotation.XmlAccessorType; |
||||
import jakarta.xml.bind.annotation.XmlElement; |
||||
import jakarta.xml.bind.annotation.XmlRootElement; |
||||
import org.junit.Rule; |
||||
import org.junit.Test; |
||||
import org.junit.rules.ExpectedException; |
||||
import java.lang.reflect.Type; |
||||
import java.util.Collection; |
||||
import java.util.Collections; |
||||
import java.util.Map; |
||||
import static feign.Util.UTF_8; |
||||
import static feign.assertj.FeignAssertions.assertThat; |
||||
import static org.junit.Assert.assertEquals; |
||||
|
||||
@SuppressWarnings("deprecation") |
||||
public class JAXBCodecTest { |
||||
|
||||
@Rule |
||||
public final ExpectedException thrown = ExpectedException.none(); |
||||
|
||||
@Test |
||||
public void encodesXml() throws Exception { |
||||
MockObject mock = new MockObject(); |
||||
mock.value = "Test"; |
||||
|
||||
RequestTemplate template = new RequestTemplate(); |
||||
new JAXBEncoder(new JAXBContextFactory.Builder().build()) |
||||
.encode(mock, MockObject.class, template); |
||||
|
||||
assertThat(template) |
||||
.hasBody( |
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><mockObject><value>Test</value></mockObject>"); |
||||
} |
||||
|
||||
@Test |
||||
public void doesntEncodeParameterizedTypes() throws Exception { |
||||
thrown.expect(UnsupportedOperationException.class); |
||||
thrown.expectMessage( |
||||
"JAXB only supports encoding raw types. Found java.util.Map<java.lang.String, ?>"); |
||||
|
||||
class ParameterizedHolder { |
||||
|
||||
Map<String, ?> field; |
||||
} |
||||
Type parameterized = ParameterizedHolder.class.getDeclaredField("field").getGenericType(); |
||||
|
||||
RequestTemplate template = new RequestTemplate(); |
||||
new JAXBEncoder(new JAXBContextFactory.Builder().build()) |
||||
.encode(Collections.emptyMap(), parameterized, template); |
||||
} |
||||
|
||||
@Test |
||||
public void encodesXmlWithCustomJAXBEncoding() throws Exception { |
||||
JAXBContextFactory jaxbContextFactory = |
||||
new JAXBContextFactory.Builder().withMarshallerJAXBEncoding("UTF-16").build(); |
||||
|
||||
Encoder encoder = new JAXBEncoder(jaxbContextFactory); |
||||
|
||||
MockObject mock = new MockObject(); |
||||
mock.value = "Test"; |
||||
|
||||
RequestTemplate template = new RequestTemplate(); |
||||
encoder.encode(mock, MockObject.class, template); |
||||
|
||||
assertThat(template).hasBody("<?xml version=\"1.0\" encoding=\"UTF-16\" " |
||||
+ "standalone=\"yes\"?><mockObject><value>Test</value></mockObject>"); |
||||
} |
||||
|
||||
@Test |
||||
public void encodesXmlWithCustomJAXBSchemaLocation() throws Exception { |
||||
JAXBContextFactory jaxbContextFactory = |
||||
new JAXBContextFactory.Builder() |
||||
.withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd") |
||||
.build(); |
||||
|
||||
Encoder encoder = new JAXBEncoder(jaxbContextFactory); |
||||
|
||||
MockObject mock = new MockObject(); |
||||
mock.value = "Test"; |
||||
|
||||
RequestTemplate template = new RequestTemplate(); |
||||
encoder.encode(mock, MockObject.class, template); |
||||
|
||||
assertThat(template).hasBody("<?xml version=\"1.0\" encoding=\"UTF-8\" " + |
||||
"standalone=\"yes\"?><mockObject xsi:schemaLocation=\"http://apihost " + |
||||
"http://apihost/schema.xsd\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">" + |
||||
"<value>Test</value></mockObject>"); |
||||
} |
||||
|
||||
@Test |
||||
public void encodesXmlWithCustomJAXBNoNamespaceSchemaLocation() throws Exception { |
||||
JAXBContextFactory jaxbContextFactory = |
||||
new JAXBContextFactory.Builder() |
||||
.withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd").build(); |
||||
|
||||
Encoder encoder = new JAXBEncoder(jaxbContextFactory); |
||||
|
||||
MockObject mock = new MockObject(); |
||||
mock.value = "Test"; |
||||
|
||||
RequestTemplate template = new RequestTemplate(); |
||||
encoder.encode(mock, MockObject.class, template); |
||||
|
||||
assertThat(template) |
||||
.hasBody( |
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" " |
||||
+ "standalone=\"yes\"?><mockObject xsi:noNamespaceSchemaLocation=\"http://apihost/schema.xsd\" " |
||||
+ "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">" |
||||
+ "<value>Test</value></mockObject>"); |
||||
} |
||||
|
||||
@Test |
||||
public void encodesXmlWithCustomJAXBFormattedOutput() { |
||||
JAXBContextFactory jaxbContextFactory = |
||||
new JAXBContextFactory.Builder().withMarshallerFormattedOutput(true).build(); |
||||
|
||||
Encoder encoder = new JAXBEncoder(jaxbContextFactory); |
||||
|
||||
MockObject mock = new MockObject(); |
||||
mock.value = "Test"; |
||||
|
||||
RequestTemplate template = new RequestTemplate(); |
||||
encoder.encode(mock, MockObject.class, template); |
||||
|
||||
// RequestTemplate always expects a UNIX style newline.
|
||||
assertThat(template).hasBody( |
||||
new StringBuilder().append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>") |
||||
.append("\n") |
||||
.append("<mockObject>") |
||||
.append("\n") |
||||
.append(" <value>Test</value>") |
||||
.append("\n") |
||||
.append("</mockObject>") |
||||
.append("\n") |
||||
.toString()); |
||||
} |
||||
|
||||
@Test |
||||
public void decodesXml() throws Exception { |
||||
MockObject mock = new MockObject(); |
||||
mock.value = "Test"; |
||||
|
||||
String mockXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><mockObject>" |
||||
+ "<value>Test</value></mockObject>"; |
||||
|
||||
Response response = Response.builder() |
||||
.status(200) |
||||
.reason("OK") |
||||
.request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) |
||||
.headers(Collections.emptyMap()) |
||||
.body(mockXml, UTF_8) |
||||
.build(); |
||||
|
||||
JAXBDecoder decoder = new JAXBDecoder(new JAXBContextFactory.Builder().build()); |
||||
|
||||
assertEquals(mock, decoder.decode(response, MockObject.class)); |
||||
} |
||||
|
||||
@Test |
||||
public void doesntDecodeParameterizedTypes() throws Exception { |
||||
thrown.expect(feign.codec.DecodeException.class); |
||||
thrown.expectMessage( |
||||
"java.util.Map is an interface, and JAXB can't handle interfaces.\n" |
||||
+ "\tthis problem is related to the following location:\n" |
||||
+ "\t\tat java.util.Map"); |
||||
|
||||
class ParameterizedHolder { |
||||
|
||||
Map<String, ?> field; |
||||
} |
||||
Type parameterized = ParameterizedHolder.class.getDeclaredField("field").getGenericType(); |
||||
|
||||
Response response = Response.builder() |
||||
.status(200) |
||||
.reason("OK") |
||||
.request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) |
||||
.headers(Collections.<String, Collection<String>>emptyMap()) |
||||
.body("<foo/>", UTF_8) |
||||
.build(); |
||||
|
||||
new JAXBDecoder(new JAXBContextFactory.Builder().build()).decode(response, parameterized); |
||||
} |
||||
|
||||
@XmlRootElement |
||||
static class Box<T> { |
||||
|
||||
@XmlElement |
||||
private T t; |
||||
|
||||
public void set(T t) { |
||||
this.t = t; |
||||
} |
||||
|
||||
} |
||||
|
||||
@Test |
||||
public void decodeAnnotatedParameterizedTypes() throws Exception { |
||||
JAXBContextFactory jaxbContextFactory = |
||||
new JAXBContextFactory.Builder().withMarshallerFormattedOutput(true).build(); |
||||
|
||||
Encoder encoder = new JAXBEncoder(jaxbContextFactory); |
||||
|
||||
Box<String> boxStr = new Box<>(); |
||||
boxStr.set("hello"); |
||||
Box<Box<String>> boxBoxStr = new Box<>(); |
||||
boxBoxStr.set(boxStr); |
||||
RequestTemplate template = new RequestTemplate(); |
||||
encoder.encode(boxBoxStr, Box.class, template); |
||||
|
||||
Response response = Response.builder() |
||||
.status(200) |
||||
.reason("OK") |
||||
.request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) |
||||
.headers(Collections.<String, Collection<String>>emptyMap()) |
||||
.body(template.body()) |
||||
.build(); |
||||
|
||||
new JAXBDecoder(new JAXBContextFactory.Builder().build()).decode(response, Box.class); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Enabled via {@link feign.Feign.Builder#dismiss404()} |
||||
*/ |
||||
@Test |
||||
public void notFoundDecodesToEmpty() throws Exception { |
||||
Response response = Response.builder() |
||||
.status(404) |
||||
.reason("NOT FOUND") |
||||
.request(Request.create(HttpMethod.GET, "/api", Collections.emptyMap(), null, Util.UTF_8)) |
||||
.headers(Collections.<String, Collection<String>>emptyMap()) |
||||
.build(); |
||||
assertThat((byte[]) new JAXBDecoder(new JAXBContextFactory.Builder().build()) |
||||
.decode(response, byte[].class)).isEmpty(); |
||||
} |
||||
|
||||
@XmlRootElement |
||||
@XmlAccessorType(XmlAccessType.FIELD) |
||||
static class MockObject { |
||||
|
||||
@XmlElement |
||||
private String value; |
||||
|
||||
@Override |
||||
public boolean equals(Object obj) { |
||||
if (obj instanceof MockObject) { |
||||
MockObject other = (MockObject) obj; |
||||
return value.equals(other.value); |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return value != null ? value.hashCode() : 0; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,151 @@
@@ -0,0 +1,151 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
package feign.jaxb; |
||||
|
||||
import feign.jaxb.mock.onepackage.AnotherMockedJAXBObject; |
||||
import feign.jaxb.mock.onepackage.MockedJAXBObject; |
||||
import jakarta.xml.bind.Marshaller; |
||||
import org.junit.Test; |
||||
import java.lang.reflect.Field; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import static org.junit.Assert.*; |
||||
|
||||
public class JAXBContextFactoryTest { |
||||
|
||||
@Test |
||||
public void buildsMarshallerWithJAXBEncodingProperty() throws Exception { |
||||
JAXBContextFactory factory = |
||||
new JAXBContextFactory.Builder().withMarshallerJAXBEncoding("UTF-16").build(); |
||||
|
||||
Marshaller marshaller = factory.createMarshaller(Object.class); |
||||
assertEquals("UTF-16", marshaller.getProperty(Marshaller.JAXB_ENCODING)); |
||||
} |
||||
|
||||
@Test |
||||
public void buildsMarshallerWithSchemaLocationProperty() throws Exception { |
||||
JAXBContextFactory factory = |
||||
new JAXBContextFactory.Builder() |
||||
.withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd") |
||||
.build(); |
||||
|
||||
Marshaller marshaller = factory.createMarshaller(Object.class); |
||||
assertEquals("http://apihost http://apihost/schema.xsd", |
||||
marshaller.getProperty(Marshaller.JAXB_SCHEMA_LOCATION)); |
||||
} |
||||
|
||||
@Test |
||||
public void buildsMarshallerWithNoNamespaceSchemaLocationProperty() throws Exception { |
||||
JAXBContextFactory factory = |
||||
new JAXBContextFactory.Builder() |
||||
.withMarshallerNoNamespaceSchemaLocation("http://apihost/schema.xsd").build(); |
||||
|
||||
Marshaller marshaller = factory.createMarshaller(Object.class); |
||||
assertEquals("http://apihost/schema.xsd", |
||||
marshaller.getProperty(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION)); |
||||
} |
||||
|
||||
@Test |
||||
public void buildsMarshallerWithFormattedOutputProperty() throws Exception { |
||||
JAXBContextFactory factory = |
||||
new JAXBContextFactory.Builder().withMarshallerFormattedOutput(true).build(); |
||||
|
||||
Marshaller marshaller = factory.createMarshaller(Object.class); |
||||
assertTrue((Boolean) marshaller.getProperty(Marshaller.JAXB_FORMATTED_OUTPUT)); |
||||
} |
||||
|
||||
@Test |
||||
public void buildsMarshallerWithFragmentProperty() throws Exception { |
||||
JAXBContextFactory factory = |
||||
new JAXBContextFactory.Builder().withMarshallerFragment(true).build(); |
||||
|
||||
Marshaller marshaller = factory.createMarshaller(Object.class); |
||||
assertTrue((Boolean) marshaller.getProperty(Marshaller.JAXB_FRAGMENT)); |
||||
} |
||||
|
||||
@Test |
||||
public void testPreloadCache() throws Exception { |
||||
|
||||
List<Class<?>> classes = Arrays.asList(String.class, Integer.class); |
||||
JAXBContextFactory factory = |
||||
new JAXBContextFactory.Builder().build(classes); |
||||
|
||||
Field f = factory.getClass().getDeclaredField("jaxbContexts"); // NoSuchFieldException
|
||||
f.setAccessible(true); |
||||
Map internalCache = (Map) f.get(factory); // IllegalAccessException
|
||||
assertFalse(internalCache.isEmpty()); |
||||
assertTrue(internalCache.size() == classes.size()); |
||||
assertNotNull(internalCache.get(new JAXBContextClassCacheKey(String.class))); |
||||
assertNotNull(internalCache.get(new JAXBContextClassCacheKey(Integer.class))); |
||||
|
||||
} |
||||
|
||||
@Test |
||||
public void testClassModeInstantiation() throws Exception { |
||||
|
||||
List<Class<?>> classes = Arrays.asList(String.class, Integer.class); |
||||
JAXBContextFactory factory = |
||||
new JAXBContextFactory.Builder() |
||||
.withJAXBContextInstantiationMode(JAXBContextInstantationMode.CLASS) |
||||
.build(classes); |
||||
|
||||
Field f = factory.getClass().getDeclaredField("jaxbContexts"); // NoSuchFieldException
|
||||
f.setAccessible(true); |
||||
Map internalCache = (Map) f.get(factory); // IllegalAccessException
|
||||
assertFalse(internalCache.isEmpty()); |
||||
assertEquals(internalCache.size(), classes.size()); |
||||
assertNotNull(internalCache.get(new JAXBContextClassCacheKey(String.class))); |
||||
assertNotNull(internalCache.get(new JAXBContextClassCacheKey(Integer.class))); |
||||
|
||||
} |
||||
|
||||
@Test |
||||
public void testPackageModeInstantiationUsingSamePackage() throws Exception { |
||||
|
||||
JAXBContextFactory factory = new JAXBContextFactory.Builder() |
||||
.withJAXBContextInstantiationMode(JAXBContextInstantationMode.PACKAGE) |
||||
.build(Arrays.asList(MockedJAXBObject.class, AnotherMockedJAXBObject.class)); |
||||
|
||||
Field f = factory.getClass().getDeclaredField("jaxbContexts"); // NoSuchFieldException
|
||||
f.setAccessible(true); |
||||
Map internalCache = (Map) f.get(factory); // IllegalAccessException
|
||||
assertFalse(internalCache.isEmpty()); |
||||
assertEquals(1, internalCache.size()); |
||||
assertNotNull(internalCache.get(new JAXBContextPackageCacheKey("feign.jaxb.mock.onepackage", |
||||
AnotherMockedJAXBObject.class.getClassLoader()))); |
||||
|
||||
} |
||||
|
||||
@Test |
||||
public void testPackageModeInstantiationUsingMultiplePackages() throws Exception { |
||||
|
||||
JAXBContextFactory factory = new JAXBContextFactory.Builder() |
||||
.withJAXBContextInstantiationMode(JAXBContextInstantationMode.PACKAGE) |
||||
.build(Arrays.asList(MockedJAXBObject.class, |
||||
feign.jaxb.mock.anotherpackage.MockedJAXBObject.class)); |
||||
|
||||
Field f = factory.getClass().getDeclaredField("jaxbContexts"); // NoSuchFieldException
|
||||
f.setAccessible(true); |
||||
Map internalCache = (Map) f.get(factory); // IllegalAccessException
|
||||
assertFalse(internalCache.isEmpty()); |
||||
assertEquals(2, internalCache.size()); |
||||
assertNotNull(internalCache.get(new JAXBContextPackageCacheKey("feign.jaxb.mock.onepackage", |
||||
MockedJAXBObject.class.getClassLoader()))); |
||||
assertNotNull(internalCache.get(new JAXBContextPackageCacheKey("feign.jaxb.mock.anotherpackage", |
||||
feign.jaxb.mock.anotherpackage.MockedJAXBObject.class.getClassLoader()))); |
||||
|
||||
|
||||
} |
||||
} |
@ -0,0 +1,161 @@
@@ -0,0 +1,161 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
package feign.jaxb.examples; |
||||
|
||||
import feign.Request; |
||||
import feign.RequestTemplate; |
||||
import javax.crypto.Mac; |
||||
import javax.crypto.spec.SecretKeySpec; |
||||
import java.net.URI; |
||||
import java.security.MessageDigest; |
||||
import java.text.SimpleDateFormat; |
||||
import java.util.Date; |
||||
import java.util.TimeZone; |
||||
import static feign.Util.UTF_8; |
||||
|
||||
// http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
|
||||
public class AWSSignatureVersion4 { |
||||
|
||||
private static final String EMPTY_STRING_HASH = |
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; |
||||
private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); |
||||
static { |
||||
iso8601.setTimeZone(TimeZone.getTimeZone("GMT")); |
||||
} |
||||
String region = "us-east-1"; |
||||
String service = "iam"; |
||||
String accessKey; |
||||
String secretKey; |
||||
|
||||
public AWSSignatureVersion4(String accessKey, String secretKey) { |
||||
this.accessKey = accessKey; |
||||
this.secretKey = secretKey; |
||||
} |
||||
|
||||
static byte[] hmacSHA256(String data, byte[] key) { |
||||
try { |
||||
String algorithm = "HmacSHA256"; |
||||
Mac mac = Mac.getInstance(algorithm); |
||||
mac.init(new SecretKeySpec(key, algorithm)); |
||||
return mac.doFinal(data.getBytes(UTF_8)); |
||||
} catch (Exception e) { |
||||
throw new RuntimeException(e); |
||||
} |
||||
} |
||||
|
||||
private static String canonicalString(RequestTemplate input, String host) { |
||||
StringBuilder canonicalRequest = new StringBuilder(); |
||||
// HTTPRequestMethod + '\n' +
|
||||
canonicalRequest.append(input.method()).append('\n'); |
||||
|
||||
// CanonicalURI + '\n' +
|
||||
canonicalRequest.append(URI.create(input.url()).getPath()).append('\n'); |
||||
|
||||
// CanonicalQueryString + '\n' +
|
||||
canonicalRequest.append(input.queryLine().substring(1)); |
||||
canonicalRequest.append('\n'); |
||||
|
||||
// CanonicalHeaders + '\n' +
|
||||
canonicalRequest.append("host:").append(host).append('\n'); |
||||
|
||||
canonicalRequest.append('\n'); |
||||
|
||||
// SignedHeaders + '\n' +
|
||||
canonicalRequest.append("host").append('\n'); |
||||
|
||||
// HexEncode(Hash(Payload))
|
||||
byte[] data = input.body(); |
||||
String bodyText = (data != null) ? new String(data, input.requestCharset()) : null; |
||||
if (bodyText != null) { |
||||
canonicalRequest.append(hex(sha256(bodyText))); |
||||
} else { |
||||
canonicalRequest.append(EMPTY_STRING_HASH); |
||||
} |
||||
return canonicalRequest.toString(); |
||||
} |
||||
|
||||
private static String toSign(String timestamp, String credentialScope, String canonicalRequest) { |
||||
StringBuilder toSign = new StringBuilder(); |
||||
// Algorithm + '\n' +
|
||||
toSign.append("AWS4-HMAC-SHA256").append('\n'); |
||||
// RequestDate + '\n' +
|
||||
toSign.append(timestamp).append('\n'); |
||||
// CredentialScope + '\n' +
|
||||
toSign.append(credentialScope).append('\n'); |
||||
// HexEncode(Hash(CanonicalRequest))
|
||||
toSign.append(hex(sha256(canonicalRequest))); |
||||
return toSign.toString(); |
||||
} |
||||
|
||||
private static String hex(byte[] data) { |
||||
StringBuilder result = new StringBuilder(data.length * 2); |
||||
for (byte b : data) { |
||||
result.append(String.format("%02x", b & 0xff)); |
||||
} |
||||
return result.toString(); |
||||
} |
||||
|
||||
static byte[] sha256(String data) { |
||||
try { |
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256"); |
||||
return digest.digest(data.getBytes(UTF_8)); |
||||
} catch (Exception e) { |
||||
throw new RuntimeException(e); |
||||
} |
||||
} |
||||
|
||||
public Request apply(RequestTemplate input) { |
||||
if (!input.headers().isEmpty()) { |
||||
throw new UnsupportedOperationException("headers not supported"); |
||||
} |
||||
if (input.body() != null) { |
||||
throw new UnsupportedOperationException("body not supported"); |
||||
} |
||||
|
||||
String host = URI.create(input.url()).getHost(); |
||||
|
||||
String timestamp; |
||||
synchronized (iso8601) { |
||||
timestamp = iso8601.format(new Date()); |
||||
} |
||||
|
||||
String credentialScope = |
||||
String.format("%s/%s/%s/%s", timestamp.substring(0, 8), region, service, "aws4_request"); |
||||
|
||||
input.query("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); |
||||
input.query("X-Amz-Credential", accessKey + "/" + credentialScope); |
||||
input.query("X-Amz-Date", timestamp); |
||||
input.query("X-Amz-SignedHeaders", "host"); |
||||
input.header("Host", host); |
||||
|
||||
String canonicalString = canonicalString(input, host); |
||||
String toSign = toSign(timestamp, credentialScope, canonicalString); |
||||
|
||||
byte[] signatureKey = signatureKey(secretKey, timestamp); |
||||
String signature = hex(hmacSHA256(toSign, signatureKey)); |
||||
|
||||
input.query("X-Amz-Signature", signature); |
||||
|
||||
return input.request(); |
||||
} |
||||
|
||||
byte[] signatureKey(String secretKey, String timestamp) { |
||||
byte[] kSecret = ("AWS4" + secretKey).getBytes(UTF_8); |
||||
byte[] kDate = hmacSHA256(timestamp.substring(0, 8), kSecret); |
||||
byte[] kRegion = hmacSHA256(region, kDate); |
||||
byte[] kService = hmacSHA256(service, kRegion); |
||||
byte[] kSigning = hmacSHA256("aws4_request", kService); |
||||
return kSigning; |
||||
} |
||||
} |
@ -0,0 +1,90 @@
@@ -0,0 +1,90 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
package feign.jaxb.examples; |
||||
|
||||
import feign.*; |
||||
import feign.jaxb.JAXBContextFactory; |
||||
import feign.jaxb.JAXBDecoder; |
||||
import jakarta.xml.bind.annotation.*; |
||||
|
||||
|
||||
public class IAMExample { |
||||
|
||||
public static void main(String... args) { |
||||
IAM iam = Feign.builder() |
||||
.decoder(new JAXBDecoder(new JAXBContextFactory.Builder().build())) |
||||
.target(new IAMTarget(args[0], args[1])); |
||||
|
||||
GetUserResponse response = iam.userResponse(); |
||||
System.out.println("UserId: " + response.result.user.id); |
||||
} |
||||
|
||||
interface IAM { |
||||
|
||||
@RequestLine("GET /?Action=GetUser&Version=2010-05-08") |
||||
GetUserResponse userResponse(); |
||||
} |
||||
|
||||
static class IAMTarget extends AWSSignatureVersion4 implements Target<IAM> { |
||||
|
||||
private IAMTarget(String accessKey, String secretKey) { |
||||
super(accessKey, secretKey); |
||||
} |
||||
|
||||
@Override |
||||
public Class<IAM> type() { |
||||
return IAM.class; |
||||
} |
||||
|
||||
@Override |
||||
public String name() { |
||||
return "iam"; |
||||
} |
||||
|
||||
@Override |
||||
public String url() { |
||||
return "https://iam.amazonaws.com"; |
||||
} |
||||
|
||||
@Override |
||||
public Request apply(RequestTemplate in) { |
||||
in.target(url()); |
||||
return super.apply(in); |
||||
} |
||||
} |
||||
|
||||
@XmlRootElement(name = "GetUserResponse", namespace = "https://iam.amazonaws.com/doc/2010-05-08/") |
||||
@XmlAccessorType(XmlAccessType.FIELD) |
||||
static class GetUserResponse { |
||||
|
||||
@XmlElement(name = "GetUserResult") |
||||
private GetUserResult result; |
||||
} |
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD) |
||||
@XmlType(name = "GetUserResult") |
||||
static class GetUserResult { |
||||
|
||||
@XmlElement(name = "User") |
||||
private User user; |
||||
} |
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD) |
||||
@XmlType(name = "User") |
||||
static class User { |
||||
|
||||
@XmlElement(name = "UserId") |
||||
private String id; |
||||
} |
||||
} |
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
@jakarta.xml.bind.annotation.XmlSchema(namespace = "https://iam.amazonaws.com/doc/2010-05-08/", |
||||
elementFormDefault = jakarta.xml.bind.annotation.XmlNsForm.QUALIFIED) |
||||
package feign.jaxb.examples; |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
package feign.jaxb.mock.anotherpackage; |
||||
|
||||
|
||||
import jakarta.xml.bind.annotation.XmlRootElement; |
||||
|
||||
@XmlRootElement(name = "anothertest") |
||||
public class MockedJAXBObject { |
||||
} |
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
package feign.jaxb.mock.anotherpackage; |
||||
|
||||
|
||||
import jakarta.xml.bind.annotation.XmlRegistry; |
||||
|
||||
@XmlRegistry |
||||
public class ObjectFactory { |
||||
|
||||
public MockedJAXBObject createMockedJAXBObject() { |
||||
return new MockedJAXBObject(); |
||||
} |
||||
} |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
package feign.jaxb.mock.onepackage; |
||||
|
||||
|
||||
import jakarta.xml.bind.annotation.XmlRootElement; |
||||
|
||||
@XmlRootElement |
||||
public class AnotherMockedJAXBObject { |
||||
} |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
package feign.jaxb.mock.onepackage; |
||||
|
||||
|
||||
import jakarta.xml.bind.annotation.XmlRootElement; |
||||
|
||||
@XmlRootElement(name = "test") |
||||
public class MockedJAXBObject { |
||||
} |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
/* |
||||
* Copyright 2012-2023 The Feign Authors |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
||||
* in compliance with the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License |
||||
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
||||
* or implied. See the License for the specific language governing permissions and limitations under |
||||
* the License. |
||||
*/ |
||||
package feign.jaxb.mock.onepackage; |
||||
|
||||
|
||||
import jakarta.xml.bind.annotation.XmlRegistry; |
||||
|
||||
@XmlRegistry |
||||
public class ObjectFactory { |
||||
|
||||
public MockedJAXBObject createMockedJAXBObject() { |
||||
return new MockedJAXBObject(); |
||||
} |
||||
|
||||
public AnotherMockedJAXBObject createAnotherMockedJAXBObject() { |
||||
return new AnotherMockedJAXBObject(); |
||||
} |
||||
} |
Loading…
Reference in new issue