I'm trying to use an enum to wrap certain error codes in my application.
public enum ErrorStatus
{
PAGE_NOT_FOUND("http 404", "lorem ipsum")
private final String code;
private final String description;
private ErrorStatus(String code, String description)
{
this.code = code;
this.description = description;
}
public String getDescription()
{
return description;
}
}
When I receive an error, it is a String with the code e.g. "http 404".
"http 404" (or any other error code) doesn't seem to be a clear and readable enum name:
http_404("page not found", "lorem ipsum")
I found the valueOf() and name() methods can't be overridden, and I want to prevent other from mistakenly using the valueOf() on the String value instead of a custom implementation with a different name.
What is the cleanest way to map the enum from and to that String value?
Edit: After reading your comment I think you are looking for something like this.
import java.util.HashMap;
import java.util.Map.Entry;
public enum ErrorStatus {
PAGE_NOT_FOUND("404", "Description for error 404");
private static final HashMap<String, ErrorStatus> ERRORS_BY_CODE;
private static final HashMap<String, ErrorStatus> ERRORS_BY_DESCR;
static {
ERRORS_BY_CODE = new HashMap<String, ErrorStatus>();
ERRORS_BY_CODE.put("404", PAGE_NOT_FOUND);
ERRORS_BY_DESCR = new HashMap<String, ErrorStatus>();
ERRORS_BY_DESCR.put("Description for error 404", PAGE_NOT_FOUND);
}
So the most important thing here is the use of HashMaps, much like ZouZou suggested. If you want to efficiently look for a description by a givwn code you'll need a map for that, if you want to efficiently look for a code by a given description you'll need a map for that too.
If you have a string like "404" or "500" and want to get the corresponding description you can use
public static ErrorStatus getErrorByCode(String code) {
return ERRORS_BY_CODE.get(code);
}
If you have the description like "Description for error 404" and want to get the corresponding error code you can use
public static ErrorStatus getErrorByDescr(String descr) {
return ERRORS_BY_DESCR.get(descr);
}
If you only have a string containing the description it gets a bit nasty. This is not the most efficient way to do it but assuming you wont have that many error codes it's all right. So if we have a string like "Here is the description of the page not found error 'Description for error 404'" then you can use
public static ErrorStatus getErrorByString(String str) {
for (Entry<String, ErrorStatus> entry : ERRORS_BY_DESCR.entrySet()){
if (str.contains(entry.getKey())) {
return entry.getValue();
}
}
return null;
}
Be carefull about the last method as it returns null if nothing was found and also only gives only the first error object it succeeds (while there can be more than one error description in a code).
You can use a map to hold the mapping code-enum.
enum ErrorStatus {
PAGE_NOT_FOUND("404", "lorem ipsum");
private static class Holder {
private static Map<String, ErrorStatus> MAP = new HashMap<>();
}
private String code;
private String description;
private ErrorStatus(String code, String description) {
this.code = code;
this.description = description;
Holder.MAP.put(code, this);
}
public static ErrorStatus fromCode(String code) {
ErrorStatus error = Holder.MAP.get(code);
if(error == null) {
throw new IllegalArgumentException();
}
return error;
}
}
and then call it like:
ErrorStatus status = ErrorStatus.fromCode("404"); //PAGE_NOT_FOUND
The trick is that the class loader initializes the static inner class before the enum class so that you can use the map in the enum constructor, which implies minimal code.
For mapping the enum to its code, you can just add a method that will give the code value:
public String code() { return this.code; }
There seems no way to prevent valueof or override it.
No it's right you cannot. But you can create a class with public static final fields.
class ErrorStatus {
public static final ErrorStatus PAGE_NOT_FOUND = new ErrorStatus("404", "lorem ipsum");
//override toString, hashCode and equals
//same code as in the enum
}
Related
This question already has answers here:
Converting many 'if else' statements to a cleaner approach [duplicate]
(7 answers)
Closed 4 years ago.
I think this is a very common situation in web projects. Assume there is an entity such as:
//JAVA code
#Data
class Entity{
private String a;
private String aExt;
private String b;
private String bExt;
private String c;
private String cExt;
... something more ...
}
For some purpose, I need to get part of values from Entity according to a passed argument, like:
public ViewObject foo(Entity entity, String condition){
ViewObject vo = new ViewObject();
if("aRelated".equals(condition)){
vo.setValue1(entity.getA());
vo.setValue2(entity.getAExt());
}
else if("bRelated".equals(condition)){
vo.setValue1(entity.getB());
vo.setValue2(entity.getBExt());
}
else if(cRelated".equals(condition)){
vo.setValue1(entity.getC());
vo.setValue2(entity.getCExt());
}
... else statement if there are other values ....
return vo;
}
I know I can use switch-case statement to reduce some words in foo(), but there is no essential difference compared with if-else, especially when the Entity has many variables.
As a plain Example, foo() is only a view object builder, but my project is more complex which have many duplicated code with only different variable's name in each if-else statement.
How do I reduce the above duplicated code?
You can try creating two hash maps:
// name these properly!
HashMap<String, Function<Entity, String>> valueMap = new HashMap<>();
HashMap<String, Function<Entity, String>> extMap = new HashMap<>();
Add these KVPs:
// valueMap
"aRelated" - Entity::getA
"bRelated" - Entity::getB
"cRelated" - Entity::getC
// extMap
"aRelated" - Entity::getAExt
"bRelated" - Entity::getBExt
"cRelated" - Entity::getCExt
Now, you can do this without an if statement:
vo.setValue1(valueMap.get(condition).apply(entity));
vo.setValue2(extMap.get(condition).apply(entity));
Another option would be to use reflection:
import java.lang.reflect.Method;
import java.lang.reflext.InvocationTargetException;
...
public ViewObject foo(Entity e, String c) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
String[] methodNames = { "get" + c.substring(0,1).toUpperCase(), "get" + c.substring(0,1).toUpperCase() + "Ext" };
Method[] methods = { e.getClass().getDeclaredMethod(methodNames[0]), e.getClass().getDeclaredMethod(methodNames[1]) };
ViewObject vo = new ViewObject();
vo.setValue1((String)methods[0].invoke(e));
vo.setValue2((String)methods[1].invoke(e));
return vo;
}
Although I have to admit I personally like the map approach of the other answers more, just showing more options.
Use of a Map would do the trick:
class EntityPart {
String s;
String sExt;
}
class Entity {
Map<String,EntityPart> m = new HashMap<>();
m.add("aRelated",new EntityPart());
m.add("bRelated",new EntityPart());
....
}
public ViewObject foo(Entity entity, String condition) {
ViewObject vo = new ViewObject();
EntityPart ep = entity.m.get(condition);
vo.setValue1(ep.s);
vo.setValue2(ep.sExt);
return vo;
}
Make Entity as enum instead of class.
public enum Entity {
A("a", "aExt"), B("b", "bExt"), C("c", "cExt");
private final String name;
private final String text;
private Entity(String name, String text) {
this.name = name;
this.text = text;
}
public String getName() {
return name;
}
public String getText() {
return text;
}
public static Entity fromString(String raw) {
return LOOKUP.get(raw);
}
private static final Map<String, Entity> LOOKUP = new HashMap<>();
static {
for (Entity e : values()) {
LOOKUP.put(e.getName(), e);
}
}
}
And modify your foo method as
public ViewObject foo(String condition){
/*
* pass condition as "a", "b", "c" only not "aRelated", "bRelated", "cRelated"
*
*/
ViewObject vo = new ViewObject();
Entity e = Entity.fromString(condition);
if(null != e) {
vo.setValue1(e.getName());
vo.setValue2(e.getText());
}
return vo;
}
i think i have a good question regarding the work i'm doing. So i want to create an object to store the type of an HTML element and his xpath. The code is very simple :
public class XpathObject {
private String type;
private String xpath;
public XpathObject(String type, String xpath) {
if(!type.equals("input") && !type.equals("label") && !type.equals("textarea") && !type.equals("button")) {
throw new IllegalArgumentException();
}
this.type = type;
this.xpath = xpath;
}
public String getType() {
return type;
}
public String getXpath() {
return xpath;
}
}
The things is that when i have a the "label" type i want this object to have the following 3 fields instead of the 2, type, xpath and xpath_bis. The naive approch woulb be to always put a the xpath_bis and ingore it when i'm not in the "label" case, but i find ugly to have a field setted to null (i might be wrong). How could i implement this case in a proper way. Thanks.
The following would be feasible
public class XpathObject {
public final String type; // Or protected final
public final String xpath;
public XpathObject(String type, String xpath) { // Or protected
this.type = type;
this.xpath = xpath;
}
}
public class XpathObjectLabel {
public final String xpathBis;
public XpathObjectLabel(String xpath, String xpathBis) {
super("label", xpath);
this.xpathBis = xpathBis;
}
}
This would make sense if one would make classes XpathObjectTextArea and such.
However as I guess you are creating these objects by parsing text, without much functionality w.r.t. special methods/logic, I think simply putting all in one single class would be a good start. With java 8 influence:
public class XpathObject {
public final String type; // Or protected final
public final String xpath;
public Optional<String> xpathBis = Optional.empty();
public XpathObject(String type, String xpath) { // Or protected
this.type = type;
this.xpath = xpath;
}
}
Reason: you probably process the DOM elements hierarchically, searching all text inputs (input text + textarea) or whatever. Then you are primarily dealing with XPathObject.
More intelligent things, like what input does this label refer to, require intelligent data structures, Map, an own FieldDefinition class or whatever.
And refactoring is easy.
First I would suggest to use an enumeration for type instead of a String. This makes it type safe and easily extensible and removes the need for the ugly if statement.
public enum ElementType {
INPUT, LABEL, TEXTAREA, BUTTON;
}
Second there should be a subclass for type LABEL that has the extra field.
Third I would suggest having Builders that have the appropriat setters and return the correct subtypes, like this:
XPathBuilder.builder(LABEL).setXpath("path").setXpathBis("bis").build();
and
XPathBuilder.builder(TEXTAREA).setXpath("path").build();
I'm trying and failing to deserialize an enum with Jackson 2.5.4, and I don't quite see my case out there. My input strings are camel case, and I want to simply map to standard Enum conventions.
#JsonFormat(shape = JsonFormat.Shape.STRING)
public enum Status {
READY("ready"),
NOT_READY("notReady"),
NOT_READY_AT_ALL("notReadyAtAll");
private static Map<String, Status> FORMAT_MAP = Stream
.of(Status.values())
.collect(toMap(s -> s.formatted, Function.<Status>identity()));
private final String formatted;
Status(String formatted) {
this.formatted = formatted;
}
#JsonCreator
public Status fromString(String string) {
Status status = FORMAT_MAP.get(string);
if (status == null) {
throw new IllegalArgumentException(string + " has no corresponding value");
}
return status;
}
}
I've also tried #JsonValue on a getter to no avail, which was an option I saw reported elsewhere. They all blow up with:
com.fasterxml.jackson.databind.exc.InvalidFormatException: Can not construct instance of ...Status from String value 'ready': value not one of declared Enum instance names: ...
What am I doing wrong?
EDIT: Starting from Jackson 2.6, you can use #JsonProperty on each element of the enum to specify its serialization/deserialization value (see here):
public enum Status {
#JsonProperty("ready")
READY,
#JsonProperty("notReady")
NOT_READY,
#JsonProperty("notReadyAtAll")
NOT_READY_AT_ALL;
}
(The rest of this answer is still valid for older versions of Jackson)
You should use #JsonCreator to annotate a static method that receives a String argument. That's what Jackson calls a factory method:
public enum Status {
READY("ready"),
NOT_READY("notReady"),
NOT_READY_AT_ALL("notReadyAtAll");
private static Map<String, Status> FORMAT_MAP = Stream
.of(Status.values())
.collect(Collectors.toMap(s -> s.formatted, Function.identity()));
private final String formatted;
Status(String formatted) {
this.formatted = formatted;
}
#JsonCreator // This is the factory method and must be static
public static Status fromString(String string) {
return Optional
.ofNullable(FORMAT_MAP.get(string))
.orElseThrow(() -> new IllegalArgumentException(string));
}
}
This is the test:
ObjectMapper mapper = new ObjectMapper();
Status s1 = mapper.readValue("\"ready\"", Status.class);
Status s2 = mapper.readValue("\"notReadyAtAll\"", Status.class);
System.out.println(s1); // READY
System.out.println(s2); // NOT_READY_AT_ALL
As the factory method expects a String, you have to use JSON valid syntax for strings, which is to have the value quoted.
This is probably a faster way to do it:
public enum Status {
READY("ready"),
NOT_READY("notReady"),
NOT_READY_AT_ALL("notReadyAtAll");
private final String formatted;
Status(String formatted) {
this.formatted = formatted;
}
#Override
public String toString() {
return formatted;
}
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
ObjectReader reader = mapper.reader(Status.class);
Status status = reader.with(DeserializationFeature.READ_ENUMS_USING_TO_STRING).readValue("\"notReady\"");
System.out.println(status.name()); // NOT_READY
}
#JsonCreator
public static Status forValue(String name)
{
return EnumUtil.getEnumByNameIgnoreCase(Status.class, name);
}
Adding this static method would resolve your problem of deserializing
For whoever is searching for enums with integer json properties. Here is what worked for me:
enum class Status (private val code: Int) {
PAST(0),
LIVE(2),
UPCOMING(1);
companion object {
private val codes = Status.values().associateBy(Status::code)
#JvmStatic #JsonCreator fun from (value: Int) = codes[value]
}
}
#JsonCreator(mode = JsonCreator.Mode.DELEGATING) was the solution for me.
https://github.com/FasterXML/jackson-module-kotlin/issues/336#issuecomment-630587525
The solutions on this page work only for single field and #JsonFormat(shape = JsonFormat.Shape.NATURAL) (default format)
this works for multiple fields and #JsonFormat(shape = JsonFormat.Shape.OBJECT)
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum PinOperationMode {
INPUT("Input", "I"),
OUTPUT("Output", "O")
;
private final String mode;
private final String code;
PinOperationMode(String mode, String code) {
this.mode = mode;
this.code = code;
}
public String getMode() {
return mode;
}
public String getCode() {
return code;
}
#JsonCreator
static PinOperationMode findValue(#JsonProperty("mode") String mode, #JsonProperty("code") String code) {
return Arrays.stream(PinOperationMode.values()).filter(pt -> pt.mode.equals(mode) && pt.code.equals(code)).findFirst().get();
}
}
You could use #JsonCreator annotation to resolve your problem. Take a look at https://www.baeldung.com/jackson-serialize-enums, there's clear enough explanation about enum and serialize-deserialize with jackson lib.
I have such enum class in java
public enum MockTypes
{
// Atlantis mocks
ATLANTIS_VERIFY("ATLANTIS", "verify"),
ATLANTIS_CREATE_RECORD("ATLANTIS", "createRecord"),
...
private String m_adaptor;
private String m_step;
private MockTypes( String adaptor, String step)
{
m_adaptor = adaptor;
m_step = step;
}
public String getAdaptor()
{
return m_adaptor;
}
public String getStep()
{
return m_step;
}
I have to implement method that returns enum value by adaptor and step parameter.
public MockTypes getMockTypeByName(String adaptor, String step)
but I have no idea how. Could someone help me?
public MockTypes getMockTypeByName(String adaptor, String step)
{
for(MockTypes m : MockTypes.values())
{
if(m.getAdaptor().equals(adaptor) &&
m.getStep().equals(step)) return m;
}
return null;
}
If you want a "constant-time" solution that doesn't involve looking up values, your best option is to initialize a constant Map in a static block in the MockType class.
If you're up for using Guava, it'll actually be relatively pleasant:
public enum MockType {
...
private static final ImmutableTable<String, String, MockType> LOOKUP_TABLE;
static {
ImmutableTable.Builder<String, String, MockType> builder =
ImmutableTable.builder();
for (MockType mockType : MockType.values()) {
builder.put(mockType.getAdaptor(), mockType.getStep(), mockType);
}
LOOKUP_TABLE = builder.build();
}
public static MockType getMockType(String adaptor, String step) {
return LOOKUP_TABLE.get(adaptor, step);
}
}
(Disclosure: I contribute to Guava.)
The alternative is going to be relatively similar -- construct a Map<String, Map<String, LookupType>> in a static block, and do lookups from there -- though it's going to require somewhat more work.
You can use enum's values() method to obtain a list of all the defined values. You can then loop through this list and find the values you're interested in that match the ones sent as parameters to the method.
The getCategory method below seems very redundant and I was wondering if anyone has some suggestions on refactoring it to make it cleaner possibly using an Enum. Based on the "val" passed in, I need getCategory to return the proper Category instance from the Category class. The Category class is generated JNI code, so I don't want to change that. Anyone have any ideas?
Method to be refactored:
private Category getCategory(String val) throws Exception{
Category category;
if (val.equalsIgnoreCase("producer")) {
usageCategory = Category.CATEGORY_PRODUCER;
} else if (val.equalsIgnoreCase("meter")) {
usageCategory = Category.CATEGORY_METER;
} else if (val.equalsIgnoreCase("consumer")) {
usageCategory = Category.CATEGORY_CONSUMER;
} else {
throw new Exception("Invalid value: " + val);
}
return usageCategory;
}
Category.java: Generated JNI (can't change this):
public final class Category {
public final static Category CATEGORY_PRODUCER = new Category("CATEGORY_PRODUCER", SampleJNI.CATEGORY_PRODUCER_get());
public final static Category CATEGORY_METER = new Category("CATEGORY_METER", SampleJNI.CATEGORY_METER_get());
public final static Category CATEGORY_CONSUMER = new Category("CATEGORY_CONSUMER", SampleJNI.CATEGORY_CONSUMER_get());
}
Your method is essentially mapping from a predetermined String to a Category, so why not use a Map instead? Specifically, I'd recommend Guava's ImmutableMap, since these mappings are static:
private static final ImmutableMap<String, Category> CATEGORIES_BY_STRING =
ImmutableMap.of(
"producer", Category.CATEGORY_PRODUCER,
"meter", Category. CATEGORY_METER,
"consumer", Category.CATEGORY_CONSUMER
);
Or the standard way if you don't want to use a third-party library:
private static final Map<String, Category> CATEGORIES_BY_STRING;
static {
Map<String, Category> backingMap = new HashMap<String, Category>();
backingMap.put("producer", Category.CATEGORY_PRODUCER);
backingMap.put("meter", Category.CATEGORY_METER);
backingMap.put("producer", Category.CATEGORY_CONSUMER);
CATEGORIES_BY_STRING = Collections.unmodifiableMap(backingMap);
}
You could still employ your method to check for invalid values (and support case-insensitivity as David Harkness pointed out):
private Category getCategory(String val) {
Category category = CATEGORIES_BY_STRING.get(val.toLowerCase());
if (category == null) {
throw new IllegalArgumentException();
}
return category;
}
About using enums:
If you have complete control over the Strings that are passed into getCategory, and would only be passing literal values, then it does make sense to switch to an enum instead.
EDIT: Previously, I recommended using an EnumMap for this case, but Adrian's answer makes much more sense.
If you said you want to refactor base on enum, I assume you mean you no longer want to pass in the String to getCategory to do all those work. The code is using enum directly, instead of using String.
If this is the case, continue reading
Luckily, your Category is static variables, so you can do something real straight-forward
public enum FooCategory { // give a better name yourself
PRODUCER(Category.CATEGORY_PRODUCER),
METER(Category.CATEGORY_METER),
CONSUMER(Category.CATEGORY_CONSUMER)
private Category category;
FooCategory(Category category) {
this.category=category;
}
Category getCategory() {
return this.category;
}
}
In your old code, you are doing something like:
String fooCategory = "producer";
//....
Category category = getCategory(fooCategory);
// work on category
Now you are doing something much neater
FooCategory fooCategory = FooCategory.PRODUCER;
//...
Category category = fooCategory.getCategory();
// work on category
#PaulBellora and #AdrianShum 's answers are both great, but I think you can't avoid using magic value just like 'producer'(case-insensitive) to produce Category for storage reason. So I'm afraid the redundant code in getCategory can't avoid either. (Unfortunately, the magic values used to init Category and get Category are not same)
Here are the code to use Enum(Java 1.5 or above):
public enum Category {
CATEGORY_PRODUCER,
CATEGORY_METER,
CATEGORY_CONSUMER;
public static Category of(String val) throws Exception {
Category usageCategory;
if (val.equalsIgnoreCase("producer")) {
usageCategory = Category.CATEGORY_PRODUCER;
} else if (val.equalsIgnoreCase("meter")) {
usageCategory = Category.CATEGORY_METER;
} else if (val.equalsIgnoreCase("consumer")) {
usageCategory = Category.CATEGORY_CONSUMER;
} else {
throw new Exception("Invalid value: " + val);
}
return usageCategory;
}
}
If you use the same magic value to produce, fetch and store, you can:
public enum Category {
CATEGORY_PRODUCER("producer"),
CATEGORY_METER("meter"),
CATEGORY_CONSUMER("consumer");
private String category;
private Category(String category) {
this.category = category;
}
public static Category of(String val) throws Exception {
Category usageCategory;
// You can use equals not equalsIgnoreCase, so you can use map to avoid redundant code.
// Because you use the same magic value everywhere.
if (val.equals("producer")) {
usageCategory = Category.CATEGORY_PRODUCER;
} else if (val.equals("meter")) {
usageCategory = Category.CATEGORY_METER;
} else if (val.equals("consumer")) {
usageCategory = Category.CATEGORY_CONSUMER;
} else {
throw new Exception("Invalid value: " + val);
}
return usageCategory;
}
}
and create a Category as:
Category category = Category.of("producer");
Of course, you have to change the code to include SampleJNI.CATEGORY_CONSUMER_get().