DateFormat Template

I post this question in CodeReview here , but no one has seen it, so I am posting here.

If this is not the place for this question, let me know and I will delete it.

I am having a problem with the date processing API in Java. I am not a very experienced programmer, and my English is very bad, sorry for that.

Problem

I work in a large project with many subsystems, and I want to create a Date Utility library to manage all the dates in the system.

Requirements

Platform

  • Java 1.6 only
  • The front end is a JSF page with RichFaces
  • Application creates reports using JasperReports
  • The back is a Spring 3.1 application
  • It is advisable to maintain a calendar and XMLGregorianCalendar (for serializing Jaxb2)

Business logic

  • The system must support six types of dates:
    • Short Date Format: dd-MM-yy
    • Date Format: dd-MM-yyyy
    • Time format (hours and minutes only): HH: mm
    • Time and Date: dd-MM-yyyy HH: mm
    • Time and Short Date: dd-MM-yy HH: mm
  • Consistency is the most important thing in design

The long format is for details, and the short format is for meshes and places with little space.

the code

API

// Format using dd-MM-yy public String asShortDate(@Nullable Date date) ; // Parse using dd-MM-yyy public Date parseShortDate(@Nullable String string) throws ParseException; // Format using dd-MM-yyyy public String asDate(@Nullable Date date); // Parse using dd-MM-yyyy public Date parseDate(@Nullable String string) throws ParseException; // Format using HH:mm public String asTime(@Nullable Date date); // Parse using HH:mm public Date parseTime(@Nullable String string) throws ParseException; // Format using dd-MM-yyyy HH:mm public String asDateTime(@Nullable Date date); // Parse using dd-MM-yyyy HH:mm public Date parseDateTime(@Nullable String string) throws ParseException; // Format using dd-MM-yy HH:mm public String asShortDateTime(@Nullable Date date) ; // Parse using dd-MM-yy HH:mm public Date parseShortDateTime(@Nullable String string) throws ParseException; 

Parser and Formatter

 protected synchronized String dateToString(Date date, String format) { if (date == null) { return EMPTY_STRING; } return getFormat(format).format(date); } /** * @param string * @return * @throws ParseException */ protected synchronized Date stringToDate(String string, String format) throws ParseException { if (string == null || EMPTY_STRING.equals(string)) { return null; } SimpleDateFormat sdf = getFormat(format); ParsePosition psp = new ParsePosition(0); if (string.length() != format.length()) { throw new ParseException("Imposible parsear", 0); } Date toRet = sdf.parse(string, psp); if (psp.getIndex() != string.length()) { throw new ParseException("Imposible parsear", psp.getIndex()); } return toRet; } /** * Provee un formato * * @param format * @return */ private SimpleDateFormat getFormat(String format) { if (!initialized) { init(); } if (!formats.containsKey(format)) { throw new IllegalArgumentException("Unknow format " + format); } return formats.get(format); } 

Other

 private synchronized void init() { if (initialized) { return; } initialized = true; formats = new HashMap<String, SimpleDateFormat>(5); formats.put(DATE_FORMAT, new SimpleDateFormat(DATE_FORMAT)); formats.put(DATE_SHORT_FORMAT, new SimpleDateFormat(DATE_SHORT_FORMAT)); formats.put(TIME_FORMAT, new SimpleDateFormat(TIME_FORMAT)); formats.put(DATETIME_FORMAT, new SimpleDateFormat(DATETIME_FORMAT)); formats.put(DATETIME_SHORT_FORMAT, new SimpleDateFormat( DATETIME_SHORT_FORMAT)); for (SimpleDateFormat sdf : formats.values()) { sdf.setLenient(false); } } 

Questions

  • I created the stringToDate, dateToString and init methods as synchronized because SimpleDateFormat is not thread safe and I only need one initialization.
  • I made DateFormat not condescending because I don't want to handle incorrect format dates.
  • Method names are intended to improve readability (since * always return String and parse * always return date).

Further development

  • JSF date converter for automatically managing all dates.
  • JSF phase listener to format all dates.
  • A @ Designation for distinguishing between different types of types

My questions

  • Is this approach correct and supported?
  • Are my thoughts correct?
  • How can I improve this API?
  • Is restricting a property right condescendingly , or is it a waste of time?
  • When I run this in parallel (100,000 threads), I see a 3.5% failure, with unusual error messages, I think this is because it is not thread safe. What is the best way to solve this problem?

Source

Thanks for reading and sorry for my poor English. Any suggestions are welcome.

+4
source share
4 answers

The question is wonderful (even if it does not correspond 100% to the site) , it is very nice to see that such efforts are made to have clean and maintained code in other places in the world too.

I would focus on the structures used, not the actual conversion, although I take into account thread safety - which is key when working with SimpleDateFormat . Therefore, to select the tool you need (for example, Joda-Time library, a mandatory recommendation on this topic)

Let's analyze the problem by taking a step back:

  • you have fixed logic to apply to the same types of input and output, but the actual operation is performed in each case
  • you want to apply a fixed set of operations

The first point invokes the Strategy pattern; it seems applicable here without any awkwardness or signs of patternitis.

 public interface IDateFormatter { String format(Date d); Date parse(String s) throws ParseException; } 

( ParseException fully applicable here semantically, and not just because this is what SimpleDateFormat throws ...)

The second point requires the use of an enumeration to store individual instances of the strategy. (I'm not sure, however, that limiting a set of elements can be called a strategy, because it is thus not extensible.)

Keep in mind that this exact code using instances of SimpleDateFormat is not thread safe! Instead of SimpleDateFormat this structure is sufficient to implement a stream safe conversion conversion.

 public enum DateFormatter implements IDateFormatter { DATE_FORMAT(DATE_FORMAT), DATE_SHORT_FORMAT(DATE_SHORT_FORMAT), TIME_FORMAT(TIME_FORMAT), DATETIME_FORMAT(DATETIME_FORMAT), DATETIME_SHORT_FORMAT(DATETIME_SHORT_FORMAT); // for single threaded environments only! // (or substitute with thread safe implementation!) private final SimpleDateFormat format; private DateFormatter(String s) { format = new SimpleDateFormat(s); } public String format(Date d) { return format.format(d); } public Date parse(String s) throws ParseException { return format.parse(s); } } 

However, to add another layer of abstraction, completely hiding the internal parts from the developers, you can wrap this using the API you provided (and, of course, in combination with the corresponding set of visibility modifiers and package settings):

 public class MainDateFormatter<T extends Enum<T> & IDateFormatter> { //This stores the class for the enum, to private final Class<T> clazz; public MainDateFormatter(Class<T> clazz) { this.clazz=clazz; } // Format using dd-MM-yy public String asShortDate(@Nullable Date date){ return DateFormatter.DATE_SHORT_FORMAT.format(date); } // Parse using dd-MM-yyy public Date parseShortDate(@Nullable String string) throws ParseException; return DateFormatter.DATE_SHORT_FORMAT.parse(string); } //... and the rest similarly } 

You also mentioned Spring: you can put this object in an application container with a Singleton scope and be sure that no one can use anything else - as this will facilitate access.

Scary multithreaded

SimpleDateFormat (and DecimalFormat , for that matter) are known to be non-streaming. In addition, ThreadLocal associated with some concerns, but in this case I would not go against them, since currently there is no "magic" here, and classes are loaded using the same instance of ClassLoader.

  public enum DateFormatter implements IDateFormatter { //... instances... // for multithreaded environments // BEWARE ClassLoader magic can break this leading to memory leak! final ThreadLocal<SimpleDateFormat> formats; //note the final! private DateFormatter(final String s) { formats = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat(s); } }; } public String format(Date d) { return formats.get().format(d); } public Date parse(String s) throws ParseException { return formats.get().parse(s); } } 

(bits and pieces taken from this answer )

How safe are the values ​​of SimpleDateFormat and DecimalFormat without a ceiling?

When I run this in parallel (100,000 threads), I see a 3.5% failure, with unusual error messages, I think this is because it is not thread safe.

One of my former colleagues did an experiment: 2 threads were enough to spoil the conversion, and get output, for example , February 31st SimpleDateFormat , and a strange output not even related to input numbers with DecimalFormat . Not surprisingly, Sonar warns of their entry into static final variables ...

The bad news is that most errors will go unnoticed until someone complains ... If strange error messages appear on the surface, this is a completely different level of the problem.

Dealing with this may include the use of ThreadLocal s (or pools of objects, if not too many service ones) if you see that creating format objects is a bottleneck, or if it is not, simply by creating "short live", strictly local instances. when the conversion will happen.

Clean API

At first, I had conflicting feelings, since your proposed API has many methods with the same signature; all use the same input to provide the same output, they share the logic. I really wrote a small class to “fix” this:

 public class DateFormatterHider<T extends Enum<T> & IDateFormatter> { private final Class<T> clazz; public DateFormatterHider(Class<T> clazz) { this.clazz=clazz; } public String format(Date d, String formatCode) { return getEnumInstance(formatCode).format(d); } public Date parse(String s, String formatCode) throws ParseException { return getEnumInstance(formatCode).parse(s); } private T getEnumInstance(String formatCode) { return Enum.valueOf(clazz,formatCode); } } 

But after I wrote this, now it was not so nice:

  • this requires string constants (I don't like them.) that need to be placed somewhere
  • The parse method has the signature parse(String,String) - it can irritate the general smell

It seems like the advantage is that it allows you to replace the implementation by providing a different enum class (for example, only in Spring XML), but this quickly turns into a nightmare: since the format code is String , it does not provide any way to track dependencies. If someone changes the enumeration and falsely names the renaming name, the code can only ever fail on Christmas night, while singing O Tannenbaum (*) with children who can quickly get two endings: getting fired, or a painful and long divorce. The code is dangerous!

So, this is - while the impressively confusing "geekish" solution, including tricky generics - is definitely no-no.

A readable, intuitive interface with the right dependencies is critical to providing a supported application.

Perform date type conversions

Developers are inventive. Another word for this is lazy - this is an undesirable view of the same. Having rules to follow often, as a rule, leads to a kind of ingenuity, providing real ways to circumvent the rules, so I would create rules in your continuous integration system to check that, for example, SimpleDateFormat not used anywhere in the code. If you need clean code over time (i.e. goooood), it should be at least a critical error. I would say that it won’t even hurt, so that he couldn’t build ...

Format guessing option

All your formats are unique in length. This makes it technically possible for this exact situation to create a procedure for guessing the format (BTW for those who read this far, would like to get b33r, I think ...), which tries to choose the correct format depending on the length of the input string and provide a method. taking one line, and still working correctly.

However , for me it belongs to the category of hidden magic . Perhaps it will be much more readable and supported so as not to have this function, since this will lead to errors that go unnoticed (for example, the data comes in the wrong format, etc.), which no one wants! (Christmas night, O Tannenbaum, children, you know the rest ...)

*: not Oh Tanenbaum , this is another story

+3
source

The preferred approach is to use standard JDK APIs or open source libraries ( joda-time ) or FastDateFormat (thread-safe, almost a replacement for SimpleDateFormat) from commons-lang

It is better to create local instances of SimpleDateFormat when necessary. Improving performance by caching may not be very big in the context of your application. This will help avoid concurrency issues. Watch this stream

If you still want to do this: -

Since the formats you want to support are a finite set, and since parsing is strict [setLenient (false)], you might consider writing custom parsing logic. Logic will be much simpler than a general class such as SimpleDateFormat. You can also avoid the format cache.

As an example (can be improved)

 import java.text.ParseException; import java.util.Date; public class DateFormat { public final static String DATE_FORMAT = "dd-MM-yyyy"; public static void main(String[] args) throws ParseException { Date date = parseDate("12-03-1927", DATE_FORMAT); System.out.println(date); } private static Date parseDate(String date, String dateFormat) throws ParseException { if(dateFormat.equals(DATE_FORMAT)) { if(date == null || date.equals("")) throw new ParseException(null, 0); if(date.length() != 10) throw new ParseException(date, 0); int day=0, month=0, year=0; try { day = Integer.parseInt(date.substring(0, 2)); month = Integer.parseInt(date.substring(3, 5)); year = Integer.parseInt(date.substring(6, 10)); } catch(NumberFormatException ex) { throw new ParseException(date + " not conforming to format " + dateFormat, 0); } if(date.charAt(2) != '-' || date.charAt(5) != '-' ) throw new ParseException(date + " not conforming to format " + dateFormat, 0); return new Date(year - 1900, month -1, day); } return null; } 

As for the API , in the current form you can synchronize your public methods. This will serve as good documentation for your API users. Since all public methods are simple wrappers for private synchronized methods, this does not affect the performance or concurrency overhead.

This Joshua Bloch talk on API Design is very good - How to create a good API and why it matters

+2
source

Stimulating a fixed set of date formats is easier to provide if this condition takes some form in the code and does not exist only on paper. So yes, I can definitely understand why you should write this API.

Syncing in a single instance of SimpleDateFormat for each format for the JVM, however, is not a good idea in a multi-threaded environment. It will be useless to synchronize threads when heavy parsing or formatting occurs.

You say you are afraid that your developers will do whatever they want and make a mess of parsing the date / time. I agree. It is rather inconsistent that SimpleDateFormat is not thread safe, and it’s nice to be reminded of new developers.

I also agree with the commentators. Switching to Joda-Time (or JSR-310 , if you, as well as the opportunity to get rid of the Joda-Time library dependency on Java 8) will not only solve the synchronization problem, but also bring you additional benefits. With Joda-Time comes a set of “Date and Time” that prevent a lot of other things that are easy to make, an expensive solution, errors (Think new Date(20,9,2013) ) and a mental model that thinks more cleanly (for example) about timepieces belts and the difference between time intervals and time of day. For example, your parseTime method returns Date , where in fact the contents of this Date is LocalTime .

I think it is a very good idea to create public constants for format strings. Then people can use them as templates for f:convertDateTime . Alternatively, you can create a factory class that will hand out formats. Let the developers decide when they can afford to create a new one and in which components with high-performance code that they would like to use in one formatting instance they got from your factory.

I found an example of such a factory class in the GWT library: DateTimeFormat . (Obviously, they have different reasons for limiting the use of date formatting.)

This is a multithreading issue. I see three alternative solutions for this, in my personal preference:

  • Use Joda-Time / JSR-310
  • Enlighten your developers. Talk to them and add a thread safety reminder to the javadoc factory methods.
  • Fix this with code: wrap SimpleDateFormats, which you hand out in instances of the wrapper wrapper class to prevent misuse. EDIT: or use FastDateFormat , which offers Krishnakumarp.
+1
source

I think you should remove the init () method and

in the getFormat (String format) method, you use the switch block:

Like this:

 SimpleDateFormat retVal = null; switch(format){ case DATE_SHORT_FORMAT:{ retVal = new SimpleDateFormat(DATE_SHORT_FORMAT); break; } //other cases default:{ throw new IllegalArgumentException("Unknow format " + format); } } return retVal; 

This approach is thread safe, simple and easy to maintain.

0
source

Source: https://habr.com/ru/post/1502529/


All Articles