ReadOnlyObjects.java
package org.kohsuke.github.example.dataobject;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nonnull;
/**
* {@link org.kohsuke.github.GHMeta} wraps the list of GitHub's IP addresses.
* <p>
* This class is used to show examples of different ways to create simple read-only data objects. For data objects that
* can be modified, perform actions, or get other objects we'll need other examples.
* <p>
* IMPORTANT: There is no one right way to do this, but there are better and worse.
* <ul>
* <li>Better: {@link GHMetaGettersUnmodifiable} is a good balance of clarity and brevity</li>
* <li>Worse: {@link GHMetaPublic} exposes setters that are not needed, making it unclear that fields are actually
* read-only</li>
* </ul>
*
* @author Liam Newman
* @see org.kohsuke.github.GHMeta
* @see <a href="https://developer.github.com/v3/meta/#meta">Get Meta</a>
*/
public final class ReadOnlyObjects {
/**
* Placeholder constructor.
*/
public ReadOnlyObjects() {
}
/**
* All GHMeta data objects should expose these values.
*
* @author Liam Newman
*/
public interface GHMetaExample {
/**
* Is verifiable password authentication boolean.
*
* @return the boolean
*/
boolean isVerifiablePasswordAuthentication();
/**
* Gets hooks.
*
* @return the hooks
*/
List<String> getHooks();
/**
* Gets git.
*
* @return the git
*/
List<String> getGit();
/**
* Gets web.
*
* @return the web
*/
List<String> getWeb();
/**
* Gets api.
*
* @return the api
*/
List<String> getApi();
/**
* Gets pages.
*
* @return the pages
*/
List<String> getPages();
/**
* Gets importer.
*
* @return the importer
*/
List<String> getImporter();
}
/**
* This version uses public getters and setters and leaves it up to Jackson how it wants to fill them.
* <p>
* Pro:
* <ul>
* <li>Easy to create</li>
* <li>Not much code</li>
* <li>Minimal annotations</li>
* </ul>
* Con:
* <ul>
* <li>Exposes public setters for fields that should not be changed, flagged by spotbugs</li>
* <li>Lists modifiable when they should not be changed</li>
* <li>Jackson generally doesn't call the setters, it just sets the fields directly</li>
* </ul>
*
* @author Paulo Miguel Almeida
* @see org.kohsuke.github.GHMeta
*/
public static class GHMetaPublic implements GHMetaExample {
/**
* Create default GHMetaPublic instance
*/
public GHMetaPublic() {
}
@JsonProperty("verifiable_password_authentication")
private boolean verifiablePasswordAuthentication;
private List<String> hooks;
private List<String> git;
private List<String> web;
private List<String> api;
private List<String> pages;
private List<String> importer;
public boolean isVerifiablePasswordAuthentication() {
return verifiablePasswordAuthentication;
}
/**
* Sets verifiable password authentication.
*
* @param verifiablePasswordAuthentication
* the verifiable password authentication
*/
public void setVerifiablePasswordAuthentication(boolean verifiablePasswordAuthentication) {
this.verifiablePasswordAuthentication = verifiablePasswordAuthentication;
}
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Noted above")
public List<String> getHooks() {
return hooks;
}
/**
* Sets hooks.
*
* @param hooks
* the hooks
*/
@SuppressFBWarnings(value = { "EI_EXPOSE_REP2" }, justification = "Spotbugs also doesn't like this")
public void setHooks(List<String> hooks) {
this.hooks = hooks;
}
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Noted above")
public List<String> getGit() {
return git;
}
/**
* Sets git.
*
* @param git
* the git
*/
@SuppressFBWarnings(value = { "EI_EXPOSE_REP2" }, justification = "Spotbugs also doesn't like this")
public void setGit(List<String> git) {
this.git = git;
}
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Noted above")
public List<String> getWeb() {
return web;
}
/**
* Sets web.
*
* @param web
* the web
*/
@SuppressFBWarnings(value = { "EI_EXPOSE_REP2" }, justification = "Spotbugs also doesn't like this")
public void setWeb(List<String> web) {
this.web = web;
}
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Noted above")
public List<String> getApi() {
return api;
}
/**
* Sets api.
*
* @param api
* the api
*/
@SuppressFBWarnings(value = { "EI_EXPOSE_REP2" }, justification = "Spotbugs also doesn't like this")
public void setApi(List<String> api) {
this.api = api;
}
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Noted above")
public List<String> getPages() {
return pages;
}
/**
* Sets pages.
*
* @param pages
* the pages
*/
@SuppressFBWarnings(value = { "EI_EXPOSE_REP2" }, justification = "Spotbugs also doesn't like this")
public void setPages(List<String> pages) {
this.pages = pages;
}
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Noted above")
public List<String> getImporter() {
return importer;
}
/**
* Sets importer.
*
* @param importer
* the importer
*/
@SuppressFBWarnings(value = { "EI_EXPOSE_REP2" }, justification = "Spotbugs also doesn't like this")
public void setImporter(List<String> importer) {
this.importer = importer;
}
}
/**
* This version uses public getters and shows that package or private setters both can be used by jackson. You can
* check this by running in debug and setting break points in the setters.
*
* <p>
* Pro:
* <ul>
* <li>Easy to create</li>
* <li>Not much code</li>
* <li>Some annotations</li>
* </ul>
* Con:
* <ul>
* <li>Exposes some package setters for fields that should not be changed, better than public</li>
* <li>Lists modifiable when they should not be changed</li>
* </ul>
*
* @author Liam Newman
* @see org.kohsuke.github.GHMeta
*/
public static class GHMetaPackage implements GHMetaExample {
/**
* Create default GHMetaPackage instance
*/
public GHMetaPackage() {
}
private boolean verifiablePasswordAuthentication;
private List<String> hooks;
private List<String> git;
private List<String> web;
private List<String> api;
private List<String> pages;
/**
* Missing {@link JsonProperty} or having it on the field will cause Jackson to ignore getters and setters.
*/
@JsonProperty
private List<String> importer;
@JsonProperty("verifiable_password_authentication")
public boolean isVerifiablePasswordAuthentication() {
return verifiablePasswordAuthentication;
}
private void setVerifiablePasswordAuthentication(boolean verifiablePasswordAuthentication) {
this.verifiablePasswordAuthentication = verifiablePasswordAuthentication;
}
@JsonProperty
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Noted above")
public List<String> getHooks() {
return hooks;
}
/**
* Setters can be private (or package local) and will still be called by Jackson. The {@link JsonProperty} can
* got on the getter or setter and still work.
*
* @param hooks
* list of hooks
*/
private void setHooks(List<String> hooks) {
this.hooks = hooks;
}
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Noted above")
public List<String> getGit() {
return git;
}
/**
* Since we mostly use Jackson for deserialization, {@link JsonSetter} is also okay, but {@link JsonProperty} is
* preferred.
*
* @param git
* list of git addresses
*/
@JsonSetter
void setGit(List<String> git) {
this.git = git;
}
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Noted above")
public List<String> getWeb() {
return web;
}
/**
* The {@link JsonProperty} can got on the getter or setter and still work.
*
* @param web
* list of web addresses
*/
void setWeb(List<String> web) {
this.web = web;
}
@JsonProperty
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Noted above")
public List<String> getApi() {
return api;
}
void setApi(List<String> api) {
this.api = api;
}
@JsonProperty
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Noted above")
public List<String> getPages() {
return pages;
}
void setPages(List<String> pages) {
this.pages = pages;
}
/**
* Missing {@link JsonProperty} or having it on the field will cause Jackson to ignore getters and setters.
*
* @return list of importer addresses
*/
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Noted above")
public List<String> getImporter() {
return importer;
}
/**
* Missing {@link JsonProperty} or having it on the field will cause Jackson to ignore getters and setters.
*
* @param importer
* list of importer addresses
*/
void setImporter(List<String> importer) {
this.importer = importer;
}
}
/**
* This version uses only public getters and returns unmodifiable lists.
*
*
* <p>
* Pro:
* <ul>
* <li>Very Easy to create</li>
* <li>Minimal code</li>
* <li>Minimal annotations</li>
* <li>Fields effectively final and lists unmodifiable</li>
* </ul>
* Con:
* <ul>
* <li>Effectively final is not quite really final</li>
* <li>If one of the lists were missing (an option member, for example), it will throw NPE but we could mitigate by
* checking for null or assigning a default.</li>
* </ul>
*
* @author Liam Newman
* @see org.kohsuke.github.GHMeta
*/
public static class GHMetaGettersUnmodifiable implements GHMetaExample {
/**
* Create default GHMetaGettersUnmodifiable instance
*/
public GHMetaGettersUnmodifiable() {
}
@JsonProperty("verifiable_password_authentication")
private boolean verifiablePasswordAuthentication;
private List<String> hooks;
private List<String> git;
private List<String> web;
private List<String> api;
private List<String> pages;
/**
* If this were an optional member, we could fill it with an empty list by default.
*/
private List<String> importer = new ArrayList<>();
public boolean isVerifiablePasswordAuthentication() {
return verifiablePasswordAuthentication;
}
public List<String> getHooks() {
return Collections.unmodifiableList(hooks);
}
public List<String> getGit() {
return Collections.unmodifiableList(git);
}
public List<String> getWeb() {
return Collections.unmodifiableList(web);
}
public List<String> getApi() {
return Collections.unmodifiableList(api);
}
public List<String> getPages() {
return Collections.unmodifiableList(pages);
}
public List<String> getImporter() {
return Collections.unmodifiableList(importer);
}
}
/**
* This version uses only public getters and returns unmodifiable lists and has final fields
* <p>
* Pro:
* <ul>
* <li>Moderate amount of code</li>
* <li>More annotations</li>
* <li>Fields final and lists unmodifiable</li>
* </ul>
* Con:
* <ul>
* <li>Extra allocations - default array lists will be replaced by Jackson (yes, even though they are final)</li>
* <li>Added constructor is annoying</li>
* <li>If this object could be refreshed or populated, then the final is misleading (and possibly buggy)</li>
* </ul>
*
* @author Liam Newman
* @see org.kohsuke.github.GHMeta
*/
public static class GHMetaGettersFinal implements GHMetaExample {
private final boolean verifiablePasswordAuthentication;
private final List<String> hooks = new ArrayList<>();
private final List<String> git = new ArrayList<>();
private final List<String> web = new ArrayList<>();
private final List<String> api = new ArrayList<>();
private final List<String> pages = new ArrayList<>();
private final List<String> importer = new ArrayList<>();
@JsonCreator
private GHMetaGettersFinal(
@JsonProperty("verifiable_password_authentication") boolean verifiablePasswordAuthentication) {
// boolean fields when final seem to be really final, so we have to switch to constructor
this.verifiablePasswordAuthentication = verifiablePasswordAuthentication;
}
public boolean isVerifiablePasswordAuthentication() {
return verifiablePasswordAuthentication;
}
public List<String> getHooks() {
return Collections.unmodifiableList(hooks);
}
public List<String> getGit() {
return Collections.unmodifiableList(git);
}
public List<String> getWeb() {
return Collections.unmodifiableList(web);
}
public List<String> getApi() {
return Collections.unmodifiableList(api);
}
public List<String> getPages() {
return Collections.unmodifiableList(pages);
}
public List<String> getImporter() {
return Collections.unmodifiableList(importer);
}
}
/**
* This version uses only public getters and returns unmodifiable lists
* <p>
* Pro:
* <ul>
* <li>Fields final and lists unmodifiable</li>
* <li>Construction behavior can be controlled - if values depended on each other or needed to be set in a specific
* order, this could do that.</li>
* <li>JsonProrperty "required" works on JsonCreator constructors - lets annotation define required values</li>
* </ul>
* Con:
* <ul>
* <li>There is no way you'd know about this without some research</li>
* <li>Specific annotations needed</li>
* <li>Nonnull annotations are misleading - null value is not checked even for "required" constructor
* parameters</li>
* <li>Brittle and verbose - not friendly to large number of fields</li>
* </ul>
*
* @author Liam Newman
* @see org.kohsuke.github.GHMeta
*/
public static class GHMetaGettersFinalCreator implements GHMetaExample {
private final boolean verifiablePasswordAuthentication;
private final List<String> hooks;
private final List<String> git;
private final List<String> web;
private final List<String> api;
private final List<String> pages;
private final List<String> importer;
/**
*
* @param hooks
* the hooks - required property works, but only on creator json properties like this, ignores
* Nonnull, checked manually
* @param git
* the git list - required property works, but only on creator json properties like this, misleading
* Nonnull annotation
* @param web
* the web list - misleading Nonnull annotation
* @param api
* the api list - misleading Nonnull annotation
* @param pages
* the pages list - misleading Nonnull annotation
* @param importer
* the importer list - misleading Nonnull annotation
* @param verifiablePasswordAuthentication
* true or false
*/
@JsonCreator
private GHMetaGettersFinalCreator(@Nonnull @JsonProperty(value = "hooks", required = true) List<String> hooks,
@Nonnull @JsonProperty(value = "git", required = true) List<String> git,
@Nonnull @JsonProperty("web") List<String> web,
@Nonnull @JsonProperty("api") List<String> api,
@Nonnull @JsonProperty("pages") List<String> pages,
@Nonnull @JsonProperty("importer") List<String> importer,
@JsonProperty("verifiable_password_authentication") boolean verifiablePasswordAuthentication) {
// to ensure a value is actually not null we still have to do a null check
Objects.requireNonNull(hooks);
this.verifiablePasswordAuthentication = verifiablePasswordAuthentication;
this.hooks = Collections.unmodifiableList(hooks);
this.git = Collections.unmodifiableList(git);
this.web = Collections.unmodifiableList(web);
this.api = Collections.unmodifiableList(api);
this.pages = Collections.unmodifiableList(pages);
this.importer = Collections.unmodifiableList(importer);
}
public boolean isVerifiablePasswordAuthentication() {
return verifiablePasswordAuthentication;
}
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Unmodifiable but spotbugs doesn't detect")
public List<String> getHooks() {
return hooks;
}
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Unmodifiable but spotbugs doesn't detect")
public List<String> getGit() {
return git;
}
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Unmodifiable but spotbugs doesn't detect")
public List<String> getWeb() {
return web;
}
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Unmodifiable but spotbugs doesn't detect")
public List<String> getApi() {
return api;
}
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Unmodifiable but spotbugs doesn't detect")
public List<String> getPages() {
return pages;
}
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Unmodifiable but spotbugs doesn't detect")
public List<String> getImporter() {
return importer;
}
}
}