Tagged: jax-rs

ResourceContext#getResource and ResourceContext#initResource work differently


Following two codes work differently, I don’t know why.

return locateOperator(
        operatorId_,
        (operatorId, operator) -> resourceContext.initResource(
                new DevicesResource()
                .operatorId(operatorId)
                .operator(operator))
);
return locateOperator(
        operatorId_,
        (operatorId, operator) -> resourceContext.getResource(
                DevicesResource.class)
        .operatorId(operatorId)
        .operator(operator)
);

The DevicesResource class has a field.

@PersistenceContext
private EntityManger entityManager

getResource properly injects while initResource don’t.

getResource

The resolved resource instance is properly initialized in the context of the current request processing scope. The scope of the resolved resource instance depends on the managing container. For resources managed by JAX-RS container the default scope is per-request.

initResource

Initialize the resource or sub-resource instance. All JAX-RS injectable fields in the resource instance will be properly initialized in the context of the current request processing scope.

Redirecting to swagger-ui from JAX-RS, v2


@Path("/swagger")
public class SwaggerResource {

    @GET
    @Path("/json")
    @Produces({MediaType.APPLICATION_JSON})
    public Response readJson() throws IOException {
        try (InputStream resource
                = getClass().getResourceAsStream("/swagger.json");
             final JsonReader reader = Json.createReader(resource)) {
            final JsonObject object = reader.readObject();
            final JsonObjectBuilder builder = Json.createObjectBuilder();
            object.entrySet().stream()
                    .filter(e -> !e.getKey().equals("host")
                                 && !e.getKey().equals("basePath")
                                 && !e.getKey().equals("schemes"))
                    .forEach(e -> builder.add(e.getKey(), e.getValue()));
            builder.add("host", uriInfo.getBaseUri().getAuthority());
            builder.add("basePath", servletContext.getContextPath()
                                    + Application_.APPLICATION_PATH);
            builder.add("schemes", Json.createArrayBuilder()
                        .add(uriInfo.getBaseUri().getScheme()));
            return Response.ok((StreamingOutput) output -> {
                try (JsonWriter writer = Json.createWriter(output)) {
                    writer.writeObject(builder.build());
                }
            }).build();
        }
    }

    @GET
    @Path("/ui")
    public Response readUi() throws IOException {
        final URI json
                = uriInfo.getBaseUriBuilder()
                .path("swagger")
                .path("json")
                .build();
        final URI swaggerUi
                = UriBuilder.fromUri(servletContext.getContextPath())
                .path("swagger-ui")
                .path("index.html")
                .queryParam("url", json).build();
        return Response.seeOther(swaggerUi).build();
    }

    @Context
    private UriInfo uriInfo;

    @Context
    private ServletContext servletContext;
}

kt ucloud storage api 문제점


kt ucloud storage api를 위한 클라이언트를 하나 만들던 중 발견한 문제점들을 기록해 둔다.

X-Auth-(User|Key)

‘X-Storage-User’는 ‘X-Auth-User’로 대체 사용이 가능하며, ‘X-Storage-Pass’ 역시
‘X-Auth-Key’ 대체 사용이 가능하며 이들간의 혼용도 가능하다.

500 에러 난다. 해결된 듯 하다.

HEAD 메서드에 Accept 가 필요하다?

아무래도 HEAD 기능을 따로 만들지 않고 GET 으로 대체하는 듯 하다.
(없어도 된다는) ?format= 을 명시적으로 붙이면 잘 되는지 모르겠지만 Accept: text/plain 으로 사용하였다. wildcard(*/*)도 되는 듯 하다.

header capitalization

“HTTP header 는 case-insensitive하니까 정상임!”. 뭐 이따구 응답밖에 없다.
제공되는 SDK로 때려봐도 사용자측에서 정상적으로 사용할 수 없다.

updating meta: {aA=A, bb=b}
retrieved meta: {Aa=A, Bb=b}

asynchronous processing?

서버측에서 asynchronous 하게 처리되는 듯 하다. 예를 들어, container에서 단 하나 뿐인 object를 삭제한 후(204) 곧바로 container를 삭제하면 때때로 409가 날라온다.

auth token!!!

이건 가장 짜증나고 어려운 문제이다.

you’re expired!!!

왜 token에 expiration을 두었는지는 대충 이해가 간다. 매번 새로운 token 을 생성해 주고 계속 쌓이는 놈들 삭제하기 귀찮았거나 token을 저장해 두고 계속 사용하는 사람들 때문에 삭제 루틴이 귀찮아서였겠지.
하지만 이 시점에, 누군지, 매우 병신같은 해결책을 만들었다. 24시간 이후 expire되는 토큰을 재사용하는 것은 상관 없는데 (클라이언트쪽에서는 상관할 바가 아닌데), 한 계정에서 다수의 토큰을 (X-Auth-New-Token 헤더를 사용해서) 생성할 수 없다. X-Auth-New-Token을 사용하는 순간 기존토큰을 사용하는 세션들은 좆된다. 이 기존 세션들이 또 다시 X-Auth-New-Token을 사용하는 순간 또 다는 세션들이 좆된다.
X-Auth-New-Token 헤더를 사용하지 않고 서버에서 주는대로 사용한다 하더라로 24시간이 되는 그 시점이 어떤 행동을 할 지 명시되지 않았다. 24-t 에 토큰을 받고 24+t 시점에 사용을 하게 될 경우 (t is minimum) 어떻게 동작하는지 의문스럽다.

use reseller?

리셀러 서비스를 사용하는 것도 동일한 문제점이 있다. 특정 사용자를 생성하고, 권한을 부여한 후, 사용한다고 해도 토큰이 expire되는 순간 어떻게 동작할 지 의문이다.

what now?

현재 가능한 시나리오는 매번 random 한 reseller user를 생성한 후 그 user에 특정 container에 대한 접근 권한(read/write)를 부여 한 후 작업을 수행한 후 그 user를 삭제하는 것이다. 이런 썅…

DIY

리셀러 서비스와 관련된 user및 권한 관리 메뉴는 없다. 제공되는 sdk에도 없다. 문의해 보면 ‘…pdf 어디어디를 참고해주세요’라는 답변만 돌아온다.

리셀러 서비스

최초 생성한 유저(admin) 계정으로 자기 자신을 삭제할 수 있다. 리셀러 서비스 날라가는 것은 물론이거니와 UI 도 먹통이 된다. 헐…… 이걸 도대체…..이렇게 만들기도 힘들겠다.

고객 문의

문의를 넣어보는 것 자체가 시간 낭비다.

i’m paying for myself!!!

이런걸 돈을 내고 사용할 수 밖에 없는 현실이 싫다. kt에 그 많은 Ph.D. 들은 뭐하고 사는지 궁금하다.

diagnosing system with jax-rs


@Path("/system/diagnoses")
public class DiagnosesResource {

    private static final Logger logger
            = getLogger(DiagnosesResource.class.getName());

    private Response response() {
        handler.flush();
        return Response.ok(baos.toByteArray(), MediaType.TEXT_PLAIN).build();
    }

    @PostConstruct
    private void onPostConstruct() {
        baos = new ByteArrayOutputStream();
        formatter = new SimpleFormatter();
        handler = new StreamHandler(baos, formatter);
        logger.addHandler(handler);
    }

    @PreDestroy
    private void onPreDestroy() {
        logger.removeHandler(handler);
        handler = null;
        formatter = null;
        baos = null;
    }

    private ByteArrayOutputStream baos;

    private Formatter formatter;

    private Handler handler;
}

Now we can add some methods for diagnosing.

@POST
@Path("/some")
@Produces({MediaType.TEXT_PLAIN})
public Response some() {
    // diagnose here with some logs
    return response();
}

Producing ImageIO capabilities through JAX-RS, v3


@Path("/miscellaneous/imageIoCapabilities")
public class ImageIoCapabilitiesResource extends BaseResource {

    @XmlEnum
    public static enum Feature {
        FileSuffix() {
            @Override
            void probe(final Collection<ImageIoCapability> capabilities) {
                stream(getReaderFileSuffixes()).forEach(
                        value -> capabilities.add(
                                new ImageIoCapability().feature(this)
                                .readable(true).value(value))
                );
                stream(getWriterFileSuffixes()).forEach(
                        value -> capabilities.add(
                                capabilities.stream()
                                .filter(e -> this == e.feature
                                             && value.equals(e.value))
                                .findFirst()
                                .orElseGet(() -> new ImageIoCapability()
                                        .feature(this).value(value)
                                ).writable(true)));
            }
        }, FormatName() {
            @Override
            void probe(final Collection<ImageIoCapability> capabilities) {
                stream(getReaderFormatNames()).forEach(
                        value -> capabilities.add(
                                new ImageIoCapability().feature(this)
                                .readable(true).value(value))
                );
                stream(getWriterFormatNames()).forEach(
                        value -> capabilities.add(
                                capabilities.stream()
                                .filter(e -> this == e.feature
                                             && value.equals(e.value))
                                .findFirst()
                                .orElseGet(() -> new ImageIoCapability()
                                        .feature(this).value(value)
                                ).writable(true)));
            }
        }, MIMEType() {
            @Override
            void probe(final Collection<ImageIoCapability> capabilities) {
                stream(getReaderMIMETypes()).forEach(value -> capabilities.add(
                        new ImageIoCapability().feature(this).readable(true)
                        .value(value))
                );
                stream(getWriterMIMETypes()).forEach(value -> capabilities.add(
                        capabilities.stream()
                        .filter(e -> this == e.feature && value.equals(e.value))
                        .findFirst()
                        .orElseGet(() -> new ImageIoCapability().feature(this)
                                .value(value)
                        ).writable(true)));
            }
        };

        abstract void probe(Collection<ImageIoCapability> capabilities);
    }

    @XmlAccessorType(XmlAccessType.NONE)
    @XmlRootElement
    public static class ImageIoCapability {

        @Override
        public String toString() {
            return super.toString() + "{"
                   + "feature=" + feature
                   + ", readable=" + readable
                   + ", writable=" + writable
                   + ", value=" + value
                   + "}";
        }

        @Override
        public int hashCode() {
            int hash = 5;
            hash = 83 * hash + Objects.hashCode(this.feature);
            hash = 83 * hash + Objects.hashCode(this.value);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final ImageIoCapability other = (ImageIoCapability) obj;
            if (!Objects.equals(this.value, other.value)) {
                return false;
            }
            if (this.feature != other.feature) {
                return false;
            }
            return true;
        }

        private ImageIoCapability feature(final Feature feature) {
            this.feature = feature;
            return this;
        }

        private ImageIoCapability readable(final boolean readable) {
            this.readable = readable;
            return this;
        }

        private ImageIoCapability writable(final boolean writable) {
            this.writable = writable;
            return this;
        }

        private ImageIoCapability value(final String value) {
            this.value = value;
            return this;
        }

        @XmlAttribute
        private Feature feature;

        @XmlAttribute
        private boolean readable;

        @XmlAttribute
        private boolean writable;

        @XmlValue
        private String value;
    }

    @GET
    @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    public Response read(@MatrixParam("feature") final List<Feature> features) {
        final Set<ImageIoCapability> capabilities = new HashSet<>();
        Arrays.stream(Feature.values()).forEach(f -> f.probe(capabilities));
        final List<ImageIoCapability> entity = capabilities.stream()
                .filter(c -> features.isEmpty() || features.contains(c.feature))
                .sorted(comparing((ImageIoCapability c) -> c.feature)
                        .thenComparing(c -> c.value))
                .collect(toList());
        final CacheControl cacheControl = new CacheControl();
        cacheControl.setMaxAge((int) TimeUnit.DAYS.toSeconds(1));
        return Response.ok(new GenericEntity<List<ImageIoCapability>>(entity) {
        }).cacheControl(cacheControl).build();
    }
}

using sub resources with jax-rs


Direct Initialization

그냥 새로 만들고 돌아가는 데에 필요한 값들을 직접 설정한다.

@Path("/parents")
public class ParentsResource {

    @Path("/{parentId: \\d+}/children");
    public SubResource resourceSubResource(
            @PathParam("parentId") final long parentId) {
        return new ChildrenResource()
                .parentId(parentId)
                .persistenceService(persistenceService);
    }

    @Inject
    private PersistenceService persistenceService;
}

ResourceContext

ResourceContext를 이용하여 생성한다. 필요한 값들은 알아서 주입된다.

@Path("/parents")
public class ParentsResource {

    @Path("/{parentId: \\d+}/children");
    public SubResource resourceSubResource(
            @PathParam("parentId") final long parentId) {
        return resourceContext.getResource(ChildrenResource.class);
    }

    @Context
    private ResourceContext resourceContext;

    @Inject
    private PersistenceService persistenceService;
}

Direct Injection

Sub Resource 자체를 직접 주입받아서 사용한다.

@Path("/parents")
public class ParentsResource {

    @Path("/{parentId: \\d+}/children");
    public SubResource resourceSubResource(
            @PathParam("parentId") final long parentId) {
        return childrenResource;
    }

    @Inject
    private PersistenceService persistenceService;

    @Inject
    private ChildrenResource childrenResource;
}

이 경우 주의할 점이 하나 있는데, childrenResource 개체가 주입되는 시점이 그 리소스가 실제로 사용될 시점보다 앞선다는 점이다. 다음과 같이 ChildrenResource 클래스에서 @PostContruct 같은 생명주기 어노테이션을 사용할 경우 없는 값을 사용할 수 있다.
다음 코드를 기반으로 /parents 혹은 /parents/1 까지만 호출 했을 경우에도 childrenResource 는 주입이 되고, 그렇게 주입되기 전에 childrenResource 자체의 모든 주입위치에도 값들이 주입되게 되는데 아래와 같이 에러를 발생할 소지가 있다.

public class ChildrenResource {

    @PostConstruct
    private void fetchMatrix() {
        //parentName // NullPointerExeption
        //    = children.getMatrixParameters().getFirst("parentName");
    }

    @PathParam("children")
    private PathSegment children; // maybe null

    private String parentName;
}

다음과 같이 Optional을 사용할 수도 있지만 그리 좋다고는 볼 수 없다.

        parentName = ofNullable(children)
                .map(v -> v.getMatrixParameters().getFirst("parentName"))
                .orElse(null);

ResourceContext 클래스를 괜히, 늦게나마, 만들어 놓은게 아닌 듯 하다.

Producing ImageIO capabilities through JAX-RS, v2


import java.util.Arrays;
import java.util.logging.Logger;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlValue;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.Response;
import javax.xml.bind.annotation.XmlEnum;
import static javax.imageio.ImageIO.getReaderFileSuffixes;
import static javax.imageio.ImageIO.getReaderFormatNames;
import static javax.imageio.ImageIO.getReaderMIMETypes;
import static javax.imageio.ImageIO.getWriterFileSuffixes;
import static javax.imageio.ImageIO.getWriterFormatNames;
import static javax.imageio.ImageIO.getWriterMIMETypes;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import static java.util.stream.Collectors.toList;
import javax.ws.rs.MatrixParam;
import static java.util.Arrays.stream;
import static java.util.Comparator.comparing;
import static java.util.logging.Logger.getLogger;
import kr.co.mediamonster.ippl.ws.rs.BaseResource;

@Path("/miscellaneous/imageIoCapabilities")
public class ImageIoCapabilitiesResource extends BaseResource {

    @XmlEnum
    public static enum Feature {
        FileSuffix() {
            @Override
            void probe(final Set<ImageIoCapability> capabilities) {
                stream(getReaderFileSuffixes()).forEach(
                        value -> capabilities.add(
                                new ImageIoCapability().feature(this)
                                .readable(true).value(value))
                );
                stream(getWriterFileSuffixes()).forEach(
                        value -> capabilities.add(
                                capabilities.stream()
                                .filter(e -> this == e.feature
                                             && value.equals(e.value))
                                .findFirst()
                                .orElseGet(() -> new ImageIoCapability()
                                        .feature(this).value(value)
                                ).writable(true)));
            }
        }, FormatName() {
            @Override
            void probe(final Set<ImageIoCapability> capabilities) {
                stream(getReaderFormatNames()).forEach(
                        value -> capabilities.add(
                                new ImageIoCapability().feature(this)
                                .readable(true).value(value))
                );
                stream(getWriterFormatNames()).forEach(
                        value -> capabilities.add(
                                capabilities.stream()
                                .filter(e -> this == e.feature
                                             && value.equals(e.value))
                                .findFirst()
                                .orElseGet(() -> new ImageIoCapability()
                                        .feature(this).value(value)
                                ).writable(true)));
            }
        }, MIMEType() {
            @Override
            void probe(final Set<ImageIoCapability> capabilities) {
                stream(getReaderMIMETypes()).forEach(value -> capabilities.add(
                        new ImageIoCapability().feature(this).readable(true)
                        .value(value))
                );
                stream(getWriterMIMETypes()).forEach(value -> capabilities.add(
                        capabilities.stream()
                        .filter(e -> this == e.feature && value.equals(e.value))
                        .findFirst()
                        .orElseGet(() -> new ImageIoCapability().feature(this)
                                .value(value)
                        ).writable(true)));
            }
        };

        abstract void probe(Set<ImageIoCapability> capabilities);
    }

    @XmlAccessorType(XmlAccessType.NONE)
    @XmlRootElement
    public static class ImageIoCapability {

        @Override
        public String toString() {
            return "ImageIoCapability{"
                   + "feature=" + feature
                   + ", readable=" + readable
                   + ", writable=" + writable
                   + ", value=" + value
                   + "}";
        }

        @Override
        public int hashCode() {
            int hash = 5;
            hash = 83 * hash + Objects.hashCode(this.feature);
            hash = 83 * hash + Objects.hashCode(this.value);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final ImageIoCapability other = (ImageIoCapability) obj;
            if (!Objects.equals(this.value, other.value)) {
                return false;
            }
            if (this.feature != other.feature) {
                return false;
            }
            return true;
        }

        ImageIoCapability feature(final Feature feature) {
            this.feature = feature;
            return this;
        }

        ImageIoCapability readable(final boolean readable) {
            this.readable = readable;
            return this;
        }

        ImageIoCapability writable(final boolean writable) {
            this.writable = writable;
            return this;
        }

        ImageIoCapability value(final String value) {
            this.value = value;
            return this;
        }

        @XmlAttribute
        private Feature feature;

        @XmlAttribute
        private boolean readable;

        @XmlAttribute
        private boolean writable;

        @XmlValue
        private String value;
    }

    @GET
    @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    public Response read(
            @MatrixParam("feature") final List<Feature> features) {
        final Set<ImageIoCapability> capabilities = new HashSet<>();
        Arrays.stream(Feature.values()).forEach(f -> f.probe(capabilities));
        return Response.ok(
                new GenericEntity<List<ImageIoCapability>>(
                        capabilities.stream()
                        .filter(c -> features.isEmpty()
                                     || features.contains(c.feature))
                        .sorted(comparing((ImageIoCapability c) -> c.feature)
                                .thenComparing(c -> c.value))
                        .collect(toList())) {
        }).build();
    }
}

Producing ImageIO capabilities through JAX-RS


@Path("/miscellaneous/imageIoCapabilities")
public class ImageIoCapabilitiesResource {

    @XmlEnum
    public static enum Feature {
        FileSuffix, FormatName, MIMEType
    }

    @XmlAccessorType(XmlAccessType.NONE)
    @XmlRootElement
    public static class ImageIoCapability {

        ImageIoCapability feature(final Feature feature) {
            this.feature = feature;
            return this;
        }

        ImageIoCapability readable(final boolean readable) {
            this.readable = readable;
            return this;
        }

        ImageIoCapability writable(final boolean writable) {
            this.writable = writable;
            return this;
        }

        ImageIoCapability value(final String value) {
            this.value = value;
            return this;
        }

        @XmlAttribute
        private Feature feature;

        @XmlAttribute
        private boolean readable;

        @XmlAttribute
        private boolean writable;

        @XmlValue
        private String value;
    }

    @GET
    @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    public Response read(@MatrixParam("feature") final Feature feature) {
        final Map<String, ImageIoCapability> map = new HashMap<>();
        stream(getReaderFileSuffixes()).forEach(
                value -> map.put(
                        Feature.FileSuffix.name() + value,
                        new ImageIoCapability().feature(Feature.FileSuffix)
                        .readable(true).value(value)));
        stream(getWriterFileSuffixes()).forEach(
                value -> ofNullable(map.putIfAbsent(
                        Feature.FileSuffix.name() + value,
                        new ImageIoCapability().feature(Feature.FileSuffix).
                        writable(true).value(value)))
                .ifPresent(previous -> previous.writable(true)));
        stream(getReaderFormatNames()).forEach(
                value -> map.put(
                        Feature.FormatName + value,
                        new ImageIoCapability().feature(Feature.FormatName)
                        .readable(true).value(value)));
        stream(getWriterFormatNames()).forEach(
                value -> ofNullable(map.putIfAbsent(
                        Feature.FormatName + value, new ImageIoCapability()
                        .feature(Feature.FormatName).writable(true)
                        .value(value)))
                .ifPresent(previous -> previous.writable(true)));
        stream(getReaderMIMETypes()).forEach(
                value -> map.putIfAbsent(
                        Feature.MIMEType + value, new ImageIoCapability()
                        .feature(Feature.MIMEType).readable(true)
                        .value(value)));
        stream(getWriterMIMETypes()).forEach(
                value -> ofNullable(map.putIfAbsent(
                        Feature.MIMEType + value,
                        new ImageIoCapability().feature(Feature.MIMEType)
                        .writable(true).value(value)))
                .ifPresent(previous -> previous.writable(true)));
        return Response.ok(
                new GenericEntity<List<ImageIoCapability>>(
                        map.values().stream()
                        .filter(c -> ofNullable(feature)
                                .map(f -> f.equals(c.feature)).orElse(true))
                        .sorted(comparing((ImageIoCapability c) -> c.feature)
                                .thenComparing(c -> c.value))
                        .collect(toList())) {
        }).build();
    }
}

$ http --json GET http://.../miscellaneous/imageIoCapabilities | python -m json.tool
[
    {
        "feature": "FileSuffix",
        "readable": true,
        "value": "wbmp",
        "writable": true
    },
    {
        "feature": "FileSuffix",
        "readable": true,
        "value": "jpeg",
        "writable": true
    },
    {
        "feature": "FileSuffix",
        "readable": true,
        "value": "bmp",
        "writable": true
    },
    {
        "feature": "FileSuffix",
        "readable": true,
        "value": "png",
        "writable": true
    },
    {
        "feature": "FileSuffix",
        "readable": true,
        "value": "jpg",
        "writable": true
    },
    {
        "feature": "FileSuffix",
        "readable": true,
        "value": "gif",
        "writable": true
    },
    {
        "feature": "FormatName",
        "readable": true,
        "value": "WBMP",
        "writable": true
    },
    {
        "feature": "FormatName",
        "readable": true,
        "value": "bmp",
        "writable": true
    },
    {
        "feature": "FormatName",
        "readable": true,
        "value": "GIF",
        "writable": true
    },
    {
        "feature": "FormatName",
        "readable": true,
        "value": "jpeg",
        "writable": true
    },
    {
        "feature": "FormatName",
        "readable": true,
        "value": "BMP",
        "writable": true
    },
    {
        "feature": "FormatName",
        "readable": true,
        "value": "gif",
        "writable": true
    },
    {
        "feature": "FormatName",
        "readable": true,
        "value": "png",
        "writable": true
    },
    {
        "feature": "FormatName",
        "readable": true,
        "value": "PNG",
        "writable": true
    },
    {
        "feature": "FormatName",
        "readable": true,
        "value": "jpg",
        "writable": true
    },
    {
        "feature": "FormatName",
        "readable": true,
        "value": "wbmp",
        "writable": true
    },
    {
        "feature": "FormatName",
        "readable": true,
        "value": "JPG",
        "writable": true
    },
    {
        "feature": "FormatName",
        "readable": true,
        "value": "JPEG",
        "writable": true
    },
    {
        "feature": "MIMEType",
        "readable": true,
        "value": "image/x-png",
        "writable": true
    },
    {
        "feature": "MIMEType",
        "readable": true,
        "value": "image/vnd.wap.wbmp",
        "writable": true
    },
    {
        "feature": "MIMEType",
        "readable": true,
        "value": "image/gif",
        "writable": true
    },
    {
        "feature": "MIMEType",
        "readable": true,
        "value": "image/bmp",
        "writable": true
    },
    {
        "feature": "MIMEType",
        "readable": true,
        "value": "image/png",
        "writable": true
    },
    {
        "feature": "MIMEType",
        "readable": true,
        "value": "image/jpeg",
        "writable": true
    }
]

$ http --json GET "http://.../miscellaneous/imageIoCapabilities;feature=MIMEType" | python -m json.tool
[
    {
        "feature": "MIMEType",
        "readable": true,
        "value": "image/x-png",
        "writable": true
    },
    {
        "feature": "MIMEType",
        "readable": true,
        "value": "image/vnd.wap.wbmp",
        "writable": true
    },
    {
        "feature": "MIMEType",
        "readable": true,
        "value": "image/gif",
        "writable": true
    },
    {
        "feature": "MIMEType",
        "readable": true,
        "value": "image/bmp",
        "writable": true
    },
    {
        "feature": "MIMEType",
        "readable": true,
        "value": "image/png",
        "writable": true
    },
    {
        "feature": "MIMEType",
        "readable": true,
        "value": "image/jpeg",
        "writable": true
    }
]

injecting sub-resource directly


다른 방법도 많지만 sub-resource를 직접 주입할 수도 있나보다.

@Path("/parents");
public class ParentsResource {

    @GET
    @Path("/{parentId: \\d+}/children")
    public ChildrenResource readChidren(
        @PathParam("parentId") final long parentId) {
        return childrenResource.parentId(parentId);
    }

    @Inject
    private ChildResource childResource;
}