| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 org.apache.struts2.components; |
| |
| import com.opensymphony.xwork2.ActionContext; |
| import com.opensymphony.xwork2.TextProvider; |
| import com.opensymphony.xwork2.util.ValueStack; |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| import org.apache.struts2.views.annotations.StrutsTag; |
| import org.apache.struts2.views.annotations.StrutsTagAttribute; |
| |
| import java.io.IOException; |
| import java.io.Writer; |
| import java.time.Instant; |
| import java.time.LocalDateTime; |
| import java.time.ZoneId; |
| import java.time.ZonedDateTime; |
| import java.time.format.DateTimeFormatter; |
| import java.time.format.FormatStyle; |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.List; |
| |
| /** |
| * <!-- START SNIPPET: javadoc --> |
| * |
| * Format Date object in different ways. |
| * <p> |
| * The date tag will allow you to format a Date in a quick and easy way. |
| * You can specify a <b>custom format</b> (eg. "dd/MM/yyyy hh:mm"), you can generate |
| * <b>easy readable notations</b> (like "in 2 hours, 14 minutes"), or you can just fall back |
| * on a <b>predefined format</b> with key 'struts.date.format' in your properties file. |
| * </p> |
| * |
| * <p> |
| * If that key is not defined, it will finally fall back to the default DateFormat.MEDIUM |
| * formatting. |
| * </p> |
| * |
| * <p> |
| * <b>Note</b>: If the requested Date object isn't found on the stack, a blank will be returned. |
| * </p> |
| * |
| * <p> |
| * Configurable attributes are: |
| * </p> |
| * |
| * <ul> |
| * <li>name</li> |
| * <li>nice</li> |
| * <li>format</li> |
| * </ul> |
| * |
| * <p> |
| * Following how the date component will work, depending on the value of nice attribute |
| * (which by default is false) and the format attribute. |
| * </p> |
| * |
| * <p> |
| * <b><u>Condition 1: With nice attribute as true</u></b> |
| * </p> |
| * <table border="1" summary=""> |
| * <tr> |
| * <td>i18n key</td> |
| * <td>default</td> |
| * </tr> |
| * <tr> |
| * <td>struts.date.format.past</td> |
| * <td>{0} ago</td> |
| * </tr> |
| * <tr> |
| * <td>struts.date.format.future</td> |
| * <td>in {0}</td> |
| * </tr> |
| * <tr> |
| * <td>struts.date.format.seconds</td> |
| * <td>an instant</td> |
| * </tr> |
| * <tr> |
| * <td>struts.date.format.minutes</td> |
| * <td>{0,choice,1#one minute|1<{0} minutes}</td> |
| * </tr> |
| * <tr> |
| * <td>struts.date.format.hours</td> |
| * <td>{0,choice,1#one hour|1<{0} hours}{1,choice,0#|1#, one minute|1<, {1} minutes}</td> |
| * </tr> |
| * <tr> |
| * <td>struts.date.format.days</td> |
| * <td>{0,choice,1#one day|1<{0} days}{1,choice,0#|1#, one hour|1<, {1} hours}</td> |
| * </tr> |
| * <tr> |
| * <td>struts.date.format.years</td> |
| * <td>{0,choice,1#one year|1<{0} years}{1,choice,0#|1#, one day|1<, {1} days}</td> |
| * </tr> |
| * </table> |
| * |
| * <p> |
| * <b><u>Condition 2: With nice attribute as false and format attribute is specified eg. dd/MM/yyyyy </u></b> |
| * </p> |
| * |
| * <p>In this case the format attribute will be used.</p> |
| * |
| * <p> |
| * <b><u>Condition 3: With nice attribute as false and no format attribute is specified </u></b> |
| * </p> |
| * <table border="1" summary=""> |
| * <tr> |
| * <td>i18n key</td> |
| * <td>default</td> |
| * </tr> |
| * <tr> |
| * <td>struts.date.format</td> |
| * <td>if one is not found DateFormat.MEDIUM format will be used</td> |
| * </tr> |
| * </table> |
| * |
| * |
| * <!-- END SNIPPET: javadoc --> |
| * |
| * <p><b>Examples</b></p> |
| * <pre> |
| * <!-- START SNIPPET: example --> |
| * <s:date name="person.birthday" format="dd/MM/yyyy" /> |
| * <s:date name="person.birthday" format="%{getText('some.i18n.key')}" /> |
| * <s:date name="person.birthday" nice="true" /> |
| * <s:date name="person.birthday" /> |
| * <!-- END SNIPPET: example --> |
| * </pre> |
| * |
| * <code>Date</code> |
| * |
| */ |
| @StrutsTag(name="date", tldBodyContent="empty", tldTagClass="org.apache.struts2.views.jsp.DateTag", description="Render a formatted date.") |
| public class Date extends ContextBean { |
| |
| private static final Logger LOG = LogManager.getLogger(Date.class); |
| /** |
| * Property name to fall back when no format is specified |
| */ |
| public static final String DATETAG_PROPERTY = "struts.date.format"; |
| /** |
| * Property name that defines the past notation (default: {0} ago) |
| */ |
| public static final String DATETAG_PROPERTY_PAST = "struts.date.format.past"; |
| private static final String DATETAG_DEFAULT_PAST = "{0} ago"; |
| /** |
| * Property name that defines the future notation (default: in {0}) |
| */ |
| public static final String DATETAG_PROPERTY_FUTURE = "struts.date.format.future"; |
| private static final String DATETAG_DEFAULT_FUTURE = "in {0}"; |
| /** |
| * Property name that defines the seconds notation (default: in instant) |
| */ |
| public static final String DATETAG_PROPERTY_SECONDS = "struts.date.format.seconds"; |
| private static final String DATETAG_DEFAULT_SECONDS = "an instant"; |
| /** |
| * Property name that defines the minutes notation (default: {0,choice,1#one minute|1<{0} minutes}) |
| */ |
| public static final String DATETAG_PROPERTY_MINUTES = "struts.date.format.minutes"; |
| private static final String DATETAG_DEFAULT_MINUTES = "{0,choice,1#one minute|1<{0} minutes}"; |
| /** |
| * Property name that defines the hours notation (default: {0,choice,1#one hour|1<{0} hours}{1,choice,0#|1#, one |
| * minute|1>, {1} minutes}) |
| */ |
| public static final String DATETAG_PROPERTY_HOURS = "struts.date.format.hours"; |
| private static final String DATETAG_DEFAULT_HOURS = "{0,choice,1#one hour|1<{0} hours}{1,choice,0#|1#, one minute|1<, {1} minutes}"; |
| /** |
| * Property name that defines the days notation (default: {0,choice,1#one day|1<{0} days}{1,choice,0#|1#, one hour|1<, |
| * {1} hours}) |
| */ |
| public static final String DATETAG_PROPERTY_DAYS = "struts.date.format.days"; |
| private static final String DATETAG_DEFAULT_DAYS = "{0,choice,1#one day|1<{0} days}{1,choice,0#|1#, one hour|1<, {1} hours}"; |
| /** |
| * Property name that defines the years notation (default: {0,choice,1#one year|1<{0} years}{1,choice,0#|1#, one |
| * day|1>, {1} days}) |
| */ |
| public static final String DATETAG_PROPERTY_YEARS = "struts.date.format.years"; |
| private static final String DATETAG_DEFAULT_YEARS = "{0,choice,1#one year|1<{0} years}{1,choice,0#|1#, one day|1<, {1} days}"; |
| |
| private String name; |
| |
| private String format; |
| |
| private boolean nice; |
| |
| private String timezone; |
| |
| public Date(ValueStack stack) { |
| super(stack); |
| } |
| |
| private TextProvider findProviderInStack() { |
| for (Object o : getStack().getRoot()) { |
| if (o instanceof TextProvider) { |
| return (TextProvider) o; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Calculates the difference in time from now to the given date, and outputs it nicely. <br> An example: <br> |
| * Now = 2006/03/12 13:38:00, date = 2006/03/12 15:50:00 will output "in 1 hour, 12 minutes". |
| * |
| * @param tp text provider |
| * @param date the date |
| * @return the date nicely |
| */ |
| public String formatTime(TextProvider tp, ZonedDateTime date) { |
| ZonedDateTime now = ZonedDateTime.now(); |
| StringBuilder sb = new StringBuilder(); |
| List<Object> args = new ArrayList<>(); |
| long secs = Math.abs(now.toEpochSecond() - date.toEpochSecond()); |
| long mins = secs / 60; |
| long sec = secs % 60; |
| int min = (int) mins % 60; |
| long hours = mins / 60; |
| int hour = (int) hours % 24; |
| int days = (int) hours / 24; |
| int day = days % 365; |
| int years = days / 365; |
| |
| if (years > 0) { |
| args.add(years); |
| args.add(day); |
| args.add(sb); |
| args.add(null); |
| sb.append(tp.getText(DATETAG_PROPERTY_YEARS, DATETAG_DEFAULT_YEARS, args)); |
| } else if (day > 0) { |
| args.add(day); |
| args.add(hour); |
| args.add(sb); |
| args.add(null); |
| sb.append(tp.getText(DATETAG_PROPERTY_DAYS, DATETAG_DEFAULT_DAYS, args)); |
| } else if (hour > 0) { |
| args.add(hour); |
| args.add(min); |
| args.add(sb); |
| args.add(null); |
| sb.append(tp.getText(DATETAG_PROPERTY_HOURS, DATETAG_DEFAULT_HOURS, args)); |
| } else if (min > 0) { |
| args.add(min); |
| args.add(sec); |
| args.add(sb); |
| args.add(null); |
| sb.append(tp.getText(DATETAG_PROPERTY_MINUTES, DATETAG_DEFAULT_MINUTES, args)); |
| } else { |
| args.add(sec); |
| args.add(sb); |
| args.add(null); |
| sb.append(tp.getText(DATETAG_PROPERTY_SECONDS, DATETAG_DEFAULT_SECONDS, args)); |
| } |
| |
| args.clear(); |
| args.add(sb.toString()); |
| if (date.isBefore(now)) { |
| // looks like this date is passed |
| return tp.getText(DATETAG_PROPERTY_PAST, DATETAG_DEFAULT_PAST, args); |
| } else { |
| return tp.getText(DATETAG_PROPERTY_FUTURE, DATETAG_DEFAULT_FUTURE, args); |
| } |
| } |
| |
| @Override |
| public boolean end(Writer writer, String body) { |
| ZonedDateTime date = null; |
| final ZoneId tz = getTimeZone(); |
| // find the name on the valueStack |
| Object dateObject = findValue(name); |
| if (dateObject instanceof java.util.Date) { |
| date = ((java.util.Date) dateObject).toInstant().atZone(tz); |
| } else if (dateObject instanceof Calendar) { |
| date = ((Calendar) dateObject).toInstant().atZone(tz); |
| } else if (dateObject instanceof Long) { |
| date = Instant.ofEpochMilli((long) dateObject).atZone(tz); |
| } else if (dateObject instanceof LocalDateTime) { |
| date = ((LocalDateTime) dateObject).atZone(tz); |
| } else if (dateObject instanceof Instant) { |
| date = ((Instant) dateObject).atZone(tz); |
| } else { |
| if (devMode) { |
| TextProvider tp = findProviderInStack(); |
| String developerNotification = ""; |
| if (tp != null) { |
| developerNotification = findProviderInStack().getText( |
| "devmode.notification", |
| "Developer Notification:\n{0}", |
| new String[]{ |
| "Expression [" + name + "] passed to <s:date/> tag which was evaluated to [" + dateObject + "](" |
| + (dateObject != null ? dateObject.getClass() : "null") + ") isn't supported!" |
| } |
| ); |
| } |
| LOG.warn(developerNotification); |
| } else { |
| LOG.debug("Expression [{}] passed to <s:date/> tag which was evaluated to [{}]({}) isn't supported!", |
| name, dateObject, (dateObject != null ? dateObject.getClass() : "null")); |
| } |
| } |
| |
| //try to find the format on the stack |
| if (format != null) { |
| format = findString(format); |
| } |
| String msg; |
| if (date != null) { |
| TextProvider tp = findProviderInStack(); |
| if (tp != null) { |
| if (nice) { |
| msg = formatTime(tp, date); |
| } else { |
| DateTimeFormatter dtf; |
| if (format == null) { |
| String globalFormat = null; |
| |
| // if the format is not specified, fall back using the |
| // defined property DATETAG_PROPERTY |
| globalFormat = tp.getText(DATETAG_PROPERTY); |
| |
| // if tp.getText can not find the property then the |
| // returned string is the same as input = |
| // DATETAG_PROPERTY |
| if (globalFormat != null |
| && !DATETAG_PROPERTY.equals(globalFormat)) { |
| dtf = DateTimeFormatter.ofPattern(globalFormat, ActionContext.getContext().getLocale()); |
| } else { |
| dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) |
| .withLocale(ActionContext.getContext().getLocale()); |
| } |
| } else { |
| dtf = DateTimeFormatter.ofPattern(format, ActionContext.getContext().getLocale()); |
| } |
| msg = dtf.format(date); |
| } |
| if (msg != null) { |
| try { |
| if (getVar() == null) { |
| writer.write(msg); |
| } else { |
| putInContext(msg); |
| } |
| } catch (IOException e) { |
| LOG.error("Could not write out Date tag", e); |
| } |
| } |
| } |
| } |
| return super.end(writer, ""); |
| } |
| |
| private ZoneId getTimeZone() { |
| ZoneId tz = ZoneId.systemDefault(); |
| if (timezone != null) { |
| timezone = stripExpressionIfAltSyntax(timezone); |
| String actualTimezone = (String) getStack().findValue(timezone, String.class); |
| if (actualTimezone != null) { |
| timezone = actualTimezone; |
| } |
| tz = ZoneId.of(timezone); |
| } |
| return tz; |
| } |
| |
| @StrutsTagAttribute(description="Date or DateTime format pattern", rtexprvalue=false) |
| public void setFormat(String format) { |
| this.format = format; |
| } |
| |
| @StrutsTagAttribute(description="Whether to print out the date nicely", type="Boolean", defaultValue="false") |
| public void setNice(boolean nice) { |
| this.nice = nice; |
| } |
| |
| @StrutsTagAttribute(description = "The specific timezone in which to format the date", required = false) |
| public void setTimezone(String timezone) { |
| this.timezone = timezone; |
| } |
| |
| /** |
| * @return the name. |
| */ |
| public String getName() { |
| return name; |
| } |
| |
| @StrutsTagAttribute(description="The date value to format", required=true) |
| public void setName(String name) { |
| this.name = name; |
| } |
| |
| /** |
| * @return the format. |
| */ |
| public String getFormat() { |
| return format; |
| } |
| |
| /** |
| * @return the nice. |
| */ |
| public boolean isNice() { |
| return nice; |
| } |
| |
| /** |
| * @return the timezone. |
| */ |
| public String getTimezone() { |
| return timezone; |
| } |
| |
| } |