Java tutorial
/* * Copyright (c) Interactive Information R & D (I2RD) LLC. * All Rights Reserved. * * This software is confidential and proprietary information of * I2RD LLC ("Confidential Information"). You shall not disclose * such Confidential Information and shall use it only in * accordance with the terms of the license agreement you entered * into with I2RD. */ package com.example.app.support.service; import com.example.app.profile.model.Profile; import com.example.app.profile.model.ProfileType; import com.example.app.profile.model.client.Client; import com.example.app.profile.model.company.Company; import com.example.app.profile.model.location.Location; import com.example.app.profile.model.membership.Membership; import com.example.app.profile.model.resource.Resource; import com.example.app.profile.model.user.User; import org.apache.commons.fileupload.FileItem; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.hibernate.Hibernate; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.mail.internet.ContentType; import javax.mail.internet.ParseException; import java.io.IOException; import java.io.ObjectStreamException; import java.io.Serializable; import java.text.SimpleDateFormat; import java.time.Duration; import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Locale; import java.util.Objects; import java.util.TimeZone; import java.util.function.Consumer; import java.util.prefs.Preferences; import net.proteusframework.cms.CmsSite; import net.proteusframework.cms.component.generator.XMLRenderer; import net.proteusframework.cms.dao.CmsFrontendDAO; import net.proteusframework.core.StringFactory; import net.proteusframework.core.hibernate.dao.EntityRetriever; import net.proteusframework.core.html.HTMLElement; import net.proteusframework.core.locale.LocaleSource; import net.proteusframework.core.locale.LocaleSourceException; import net.proteusframework.core.locale.LocalizedObjectKey; import net.proteusframework.core.locale.TransientLocalizedObjectKey; import net.proteusframework.core.mail.support.MimeTypeUtility; import net.proteusframework.core.net.ContentTypes; import net.proteusframework.core.spring.ApplicationContextUtils; import net.proteusframework.data.filesystem.FileEntity; import net.proteusframework.internet.http.resource.ClassPathResourceLibraryHelper; import net.proteusframework.internet.http.resource.FactoryResource; import net.proteusframework.ui.management.ApplicationFunction; import net.proteusframework.ui.management.ApplicationRegistry; import net.proteusframework.ui.miwt.component.Component; import net.proteusframework.ui.miwt.component.Container; import net.proteusframework.ui.miwt.component.Dialog; import net.proteusframework.ui.miwt.component.HTMLComponent; import net.proteusframework.ui.miwt.component.ParentComponent; import net.proteusframework.ui.miwt.component.composite.editor.ValueEditor; import net.proteusframework.ui.miwt.event.Event; import net.proteusframework.ui.miwt.util.ComponentTreeIterator; import net.proteusframework.users.model.Name; import net.proteusframework.users.model.Principal; import net.proteusframework.users.model.Role; import net.proteusframework.users.model.TokenCredentials; import net.proteusframework.users.model.dao.PrincipalContactUtil; import net.proteusframework.users.model.dao.PrincipalDAO; import net.proteusframework.users.model.dao.RoleDAO; import static java.util.Optional.ofNullable; import static net.proteusframework.ui.miwt.component.Container.of; /** * Util class for holding utility methods and instances for use across application. * * @author Alan Holt (aholt@venturetech.net) * @since 11/2/15 11:03 AM */ @SuppressWarnings("unused") @org.springframework.stereotype.Component public class AppUtil implements Serializable { /** Preferences Key For The Profile Site. */ public static final String PREF_KEY_PROFILE_SITE = "ProfileSite.Id"; /** Preferences Key for the Frontend Access Role */ public static final String PREF_KEY_FRONTEND_ROLE = "Frontend.Role.ProgrammaticId"; /** Preferences Key for the Admin Access Role */ public static final String PREF_KEY_ADMIN_ROLE = "Admin.Role.ProgrammaticId"; /** UTC. */ public static final TimeZone UTC = TimeZone.getTimeZone(ZoneOffset.UTC); /** Logger. */ private static final Logger _logger = LogManager.getLogger(AppUtil.class); private static final long serialVersionUID = -6831853311031034991L; private static final String CLIENT_PROP_DIALOGS_ANCESTRY = "app-util-dialog-ancestor-workaround"; @SuppressWarnings("ConstantConditions") private static class Holder { final static AppUtil INSTANCE; static { INSTANCE = ApplicationContextUtils.getInstance().getContext().getBean(AppUtil.class); } } @Autowired private transient ClassPathResourceLibraryHelper _classPathResourceLibraryHelper; @Autowired private transient RoleDAO _roleDAO; @Autowired private transient PrincipalDAO _principalDAO; @Autowired private transient CmsFrontendDAO _cmsFrontendDAO; @Autowired private transient EntityRetriever _entityRetriever; @Value("${system.sender}") private String _systemSender; @Autowired @Qualifier("localeSource") private LocaleSource _localeSource; /** * Gets instance. * * @return the instance */ public static AppUtil getInstance() { return Holder.INSTANCE; } /** * Gets system sender. * * @return the system sender */ public String getSystemSender() { return _systemSender; } /** * Copy localized object key. * * @param toCopy the LOK to copy. * * @return the copy as a transient localized object key. */ @Nullable @Contract("null->null;!null->!null") public TransientLocalizedObjectKey copyLocalizedObjectKey(@Nullable LocalizedObjectKey toCopy) { if (LocalizedObjectKey.isNull(toCopy) && !(toCopy instanceof TransientLocalizedObjectKey)) return null; try { TransientLocalizedObjectKey tlok = toCopy instanceof TransientLocalizedObjectKey ? (TransientLocalizedObjectKey) toCopy : TransientLocalizedObjectKey.getTransientLocalizedObjectKey(_localeSource, toCopy); if (tlok != null) return new TransientLocalizedObjectKey(tlok.getText()); else return new TransientLocalizedObjectKey(null); } catch (LocaleSourceException e) { throw new IllegalStateException("Unable to get LOK data.", e); } } /** * Add the given value to the given collection, returning the modified collection * * @param list the collection * @param val the value * @param <C> the type of the collection * @param <T> the type in the collection * @param <V> the type of the value -- must be T or a subclass of T * * @return the collection */ public static <T, V extends T, C extends Collection<T>> C add(C list, V val) { list.add(val); return list; } /** * Add the given object into the given list only if the object is not already within the list. * * @param <V> the type of the list * @param list the List to add the object to * @param object the object to add to the list * @param index optional index to add the object at within the list. If the object is already added in the list, ensures * that the object is placed at the given index, if one is specified. * * @return the list. */ public static <V> List<V> addIfNotContains(List<V> list, V object, @Nullable Integer index) { if (!list.contains(object)) { list.add(object); } if (index != null) { list.remove(object); list.add(index, object); } return list; } /** * Converts the given information into a Bootstrap Card * * @param htmlClassName a defining classname * @param titleComponent the Component used as the card title * @param contentComponent the Component used as the card content * * @return the cardified container */ public static Container cardify(String htmlClassName, Component titleComponent, Component contentComponent) { return cardify(null, htmlClassName, titleComponent, contentComponent); } /** * Converts the given information into a Bootstrap Card * * @param container the container to convert into a Card * @param htmlClassName a defining classname * @param titleComponent the Component used as the card title * @param contentComponent the Component used as the card content * * @return the cardified container */ public static Container cardify(@Nullable Container container, String htmlClassName, Component titleComponent, Component contentComponent) { if (container == null) { return of("card " + htmlClassName, of("card-block card-header" + htmlClassName + "-header", titleComponent.addClassName("card-title")), contentComponent.addClassName("card-block")).withHTMLElement(HTMLElement.section); } else { container.removeAllComponents(); container.addClassName("card"); container.add(of("card-block card-header" + htmlClassName + "-header", titleComponent.addClassName("card-title"))); container.add(contentComponent.addClassName("card-block")); return container.withHTMLElement(HTMLElement.section); } } /** * Convert the given ZonedDateTime to a UTC date for persistence * * @param dt the ZonedDateTime to convert to UTC * * @return a Date object that represents the same instant as the ZonedDateTime, but at UTC. */ @Nullable @Contract(value = "!null->!null;null->null", pure = true) public static Date convertForPersistence(@Nullable ZonedDateTime dt) { if (dt == null) return null; ZonedDateTime atUtc = dt.withZoneSameInstant(ZoneOffset.UTC); return new Date(atUtc.toInstant().toEpochMilli()); } /** * Convert the given Date from UTC to a ZonedDateTime at the given TimeZone * * @param date the UTC date * @param zone the TimeZone to convert the time to * * @return a ZonedDateTime that represents the same instant as the UTC date, but at the given TimeZone. */ @Nullable @Contract(value = "null,_->null;_,null->null;!null,!null->!null", pure = true) public static ZonedDateTime convertFromPersisted(@Nullable Date date, @Nullable TimeZone zone) { if (date == null || zone == null) return null; ZonedDateTime from = ZonedDateTime.ofInstant(date.toInstant(), ZoneOffset.UTC); return from.withZoneSameInstant(zone.toZoneId()); } /** * Create an HTMLComponent from content that might have links requiring externalization. * * @param internalMarkup the content * * @return the component */ public static HTMLComponent createHTMLComponentFromInternalMarkup(String internalMarkup) { try { internalMarkup = XMLRenderer.parseWithRoot(internalMarkup, Event.getRequest(), Event.getResponse()); } catch (IOException e) { _logger.error("Cannot parse XHTML content to externalize links", e); } return new HTMLComponent(internalMarkup); } /** * Disable tooltip support. * * @param component the component. */ public static void disableTooltip(Component component) { component.removeClassName("tooltips"); } /** * Check if the two Double values are equal by checking that the absolute value between them is less than 0.01 * * @param d1 first double * @param d2 second double * * @return boolean, if true, the doubles are equal */ public static boolean doubleEquals(Double d1, Double d2) { return Math.abs(d1 - d2) <= 0.01; } /** * Enable tooltip support. * * @param component the component. */ public static void enableTooltip(Component component) { component.addClassName("tooltips"); } /** * Get the content type for a {@link FileItem} correcting it based on the file name if the browser didn't provide a * content type. * * @param item the item * * @return the content type */ @Nonnull public static String getContentType(@Nonnull FileItem item) { String ct = item.getContentType(); if (ct == null || "application/octet-stream".equals(ct)) ct = MimeTypeUtility.getInstance().getContentType(item.getName()); // In case MimeTypeUtility doesn't do what we wish, it has no API contract. if (ct == null) ct = "application/octet-stream"; return ct; } /** * Get the date format fixed to UTC. * * @param locale the locale. * * @return the date format. */ public static SimpleDateFormat getDateFormat(Locale locale) { return getDateFormat(locale, null); } /** * Get the date format fixed to UTC. * * @param locale the locale. * @param pattern the date format pattern * * @return the date format. */ public static SimpleDateFormat getDateFormat(Locale locale, @Nullable String pattern) { final SimpleDateFormat format = new SimpleDateFormat(ofNullable(pattern).orElse("MMM d, yyyy"), locale); format.setTimeZone(UTC); return format; } /** * Get a SimpleDateFormat to use for rendering date information within a time tag's datetime attribute * * @return SimpleDateFormat */ @Contract(" -> !null") public static SimpleDateFormat getDateTimeAttributeDateFormat() { return new SimpleDateFormat("yyyy-MM-dd"); } /** * Get a DateTimeFormatter to use for rendering time information within a time tag's datetime attribute * * @return a DateTimeFormatter */ @Nonnull public static DateTimeFormatter getDateTimeAttributeTimeFormat() { return DateTimeFormatter.ofPattern("hh:mm a"); } /** * Get the file extension for the given {@link FileItem}. * * @param file the file to retrieve the file extension for * * @return the file extension (example: ".jpg") */ @Nonnull public static String getExtensionWithDot(FileItem file) { String ext = getExtension(file); return '.' + ext; } /** * Get the file extension for the given {@link FileItem}. * * @param file the file to retrieve the file extension for * * @return the file extension (example: "jpg") */ @Nonnull public static String getExtension(FileItem file) { return _getExtensionWithFallback(file.getName(), file.getContentType()); } @NotNull private static String _getExtensionWithFallback(String fileName, String contentType) { String ext = StringFactory.getExtension(fileName); if (ext.isEmpty() && !Objects.equals(ContentTypes.Application.octet_stream.toString(), contentType)) { try { ext = new ContentType(contentType).getSubType().toLowerCase(); switch (ext) { case "jpeg": ext = "jpg"; break; case "tiff": ext = "tif"; break; case "svg+xml": ext = "svg"; break; case "x-portable-anymap": ext = "pnm"; break; case "x-portable-bitmap": ext = "pbm"; break; case "x-portable-graymap": ext = "pgm"; break; case "x-portable-pixmap": ext = "ppm"; break; default: break; } } catch (ParseException e) { _logger.error("Unable to parse content type: " + contentType, e); } } return ext; } /** * Get the file extension for the given {@link FileEntity}. * * @param file the file to retrieve the file extension for * * @return the file extension (example: ".jpg") */ @Nonnull public static String getExtensionWithDot(FileEntity file) { String ext = getExtension(file); return '.' + ext; } /** * Get the file extension for the given {@link FileEntity}. * * @param file the file to retrieve the file extension for * * @return the file extension (example: "jpg") */ @Nonnull public static String getExtension(FileEntity file) { return _getExtensionWithFallback(file.getName(), file.getContentType()); } /** * Get the text/html content type * * @return content type */ public static ContentType getHtmlContentType() { try { return new ContentType("text/html"); } catch (ParseException e) { _logger.error("Unable to create html content type", e); throw new RuntimeException(e); } } /** * Get the Modification State for the given component by utilizing a {@link ComponentTreeIterator} * * @param component the component that will serve as the root of the {@link ComponentTreeIterator} * * @return the modification state */ public static ValueEditor.ModificationState getModificationStateForComponent(Component component) { ComponentTreeIterator treeIt = new ComponentTreeIterator(component, true, true, false); while (treeIt.hasNext()) { final Component next = treeIt.next(); if (next == component) continue; if (next instanceof ValueEditor<?>) { final ValueEditor<?> editor = (ValueEditor<?>) next; if (editor.getModificationState().isModified()) return ValueEditor.ModificationState.CHANGED; } } return ValueEditor.ModificationState.UNCHANGED; } /** * Get a valid source component from the component path. This is similar to * {@link ApplicationRegistry#getValidSourceComponent(Component)} but is able to find the source component even * when a pesky dialog is in the in way as long as the dialog had {@link #recordDialogsAncestorComponent(Dialog, Component)} * call on it. * * @param component the component to start the search on * * @return a valid source component that has an ApplicationFunction annotation. * * @throws IllegalArgumentException if a valid source component could not be found. */ @Nonnull public static Component getValidSourceComponentAcrossDialogs(Component component) { LinkedList<Component> path = new LinkedList<>(); do { path.add(component); if (component.getClientProperty(CLIENT_PROP_DIALOGS_ANCESTRY) instanceof Component) component = (Component) component.getClientProperty(CLIENT_PROP_DIALOGS_ANCESTRY); else component = component.getParent(); } while (component != null); final ListIterator<Component> listIterator = path.listIterator(path.size()); while (listIterator.hasPrevious()) { final Component previous = listIterator.previous(); if (previous.getClass().isAnnotationPresent(ApplicationFunction.class)) return previous; } throw new IllegalArgumentException("Component path does not contain an application function."); } /** * Get a ZonedDateTime for comparison on membership dates * * @param zone the TimeZone * * @return the ZonedDateTime */ public static ZonedDateTime getZonedDateTimeForComparison(TimeZone zone) { ZonedDateTime dt = ZonedDateTime.now(zone.toZoneId()); dt = dt.plus(1L, ChronoUnit.HOURS); dt = dt.truncatedTo(ChronoUnit.HOURS); return dt; } /** * Initialize hibernate entity. Useful for ValueEditors. * * @param value the value. */ public static void initialize(@Nullable Membership value) { if (value == null) return; Hibernate.initialize(value); Hibernate.initialize(value.getMembershipType()); initialize(value.getUser()); } /** * Initialize hibernate entity. Useful for ValueEditors. * * @param value the value. */ public static void initialize(User value) { Hibernate.initialize(value); EntityRetriever er = EntityRetriever.getInstance(); Hibernate.initialize(er.reattachIfNecessary(value.getPrincipal())); } /** * Initialize. * * @param value the value */ public static void initialize(Profile value) { Hibernate.initialize(value); Hibernate.initialize(value.getRepository()); initialize(value.getProfileType()); } /** * Initialize. * * @param value the value */ public static void initialize(Company value) { initialize((Profile) value); Hibernate.initialize(value.getEmailLogo()); Hibernate.initialize(value.getHostname()); Hibernate.initialize(value.getImage()); initialize(value.getPrimaryLocation()); Hibernate.initialize(value.getProfileTerms()); value.getLocations().forEach(AppUtil::initialize); } /** * Initialize. * * @param value the value */ public static void initialize(Client value) { initialize((Profile) value); Hibernate.initialize(value.getLogo()); initialize(value.getPrimaryLocation()); value.getLocations().forEach(AppUtil::initialize); } /** * Initialize. * * @param value the value */ public static void initialize(Location value) { initialize((Profile) value); Hibernate.initialize(value.getAddress()); Hibernate.initialize(value.getEmailAddress()); Hibernate.initialize(value.getPhoneNumber()); } /** * Initialize hibernate entity. Useful for ValueEditors. * * @param value the value. */ public static void initialize(@Nullable ProfileType value) { if (value == null) return; Hibernate.initialize(value); Hibernate.initialize(value.getMembershipTypeSet()); value.getMembershipTypeSet().forEach(Hibernate::initialize); } /** * Check if the provided HTML content has anything visible to present to a user. * * @param markup the markup * * @return true if there is something to show */ public static boolean isEmptyMarkup(String markup) { final Document document = Jsoup.parse(markup); return document.text().trim().isEmpty(); } /** * Add a null element as the first element of the list. * * @param <T> the type. * @param list the list. * * @return the list. */ public static <T> List<T> nullFirst(Collection<T> list) { ArrayList<T> tList = new ArrayList<>(list); return nullInIndex(0, tList); } /** * Add a null element as the element in the given index of the list. * * @param <T> the type. * @param index the index to add the null element in at * @param list the list * * @return the list */ public static <T> List<T> nullInIndex(int index, List<T> list) { if (list.isEmpty() || list.get(0) != null) list.add(index, null); return list; } /** * Add a null element as the first element of the list. * * @param <T> the type. * @param values the values that will make up the list * * @return the list. */ @SuppressWarnings("unchecked") public static <T> List<T> nullFirst(@Nonnull T... values) { return nullInIndex(0, values); } /** * Add a null element as the element in the given index of the list. * * @param <T> the type. * @param values the values that will make up the list * @param index the index to add the null element in at * * @return the list */ @SuppressWarnings("unchecked") public static <T> List<T> nullInIndex(int index, @Nonnull T... values) { return nullInIndex(index, new ArrayList<>(Arrays.asList(values))); } /** * Record which component is the ancestor (effectively) for a Dialog. * See {@link #getValidSourceComponentAcrossDialogs(Component)} for why you might want to record this. * * @param dlg the dialog * @param ancestor the ancestor */ public static void recordDialogsAncestorComponent(Dialog dlg, Component ancestor) { dlg.putClientProperty(CLIENT_PROP_DIALOGS_ANCESTRY, ancestor); } /** * Render a user's info in a way that identifies their user account and who the person is * * @param u the user * * @return the rendered info */ public static String renderUser(@Nonnull User u) { Name n = PrincipalContactUtil.getName(u.getPrincipal()); if (n == null) return "User#" + u.getId(); return n.getFirst() + ' ' + n.getLast() + " (" + u.getPrincipal().getPasswordCredentials().getUsername() + ')'; } /** * Get the default time zone. * * @return the default time zone. */ public TimeZone getDefaultTimeZone() { return getSite().getDefaultTimeZone(); } /** * Get the site. * * @return the site. */ public CmsSite getSite() { long siteId = Preferences.systemRoot().getLong(PREF_KEY_PROFILE_SITE, 0); final CmsSite site = _cmsFrontendDAO.getSite(siteId); assert site != null; return site; } /** * Convert the given ZonedDateTime to a Date * * @param dt the ZonedDateTime * * @return a Date object that represents the same instant as the ZonedDateTime, at the ZonedDateTime's timezone. */ @Nullable public static Date toDate(@Nullable ZonedDateTime dt) { if (dt == null) return null; return new Date(dt.toInstant().toEpochMilli()); } /** * Convert the given Date to a ZonedDateTime at the given TimeZone * * @param date the Date * @param zone the TimeZone to convert to * * @return a ZonedDateTime that represents the same instant as the Date, at the TimeZone specified. */ @Nullable public ZonedDateTime toZonedDateTime(@Nullable Date date, @Nullable TimeZone zone) { if (date == null || zone == null) return null; return ZonedDateTime.ofInstant(date.toInstant(), zone.toZoneId()); } /** * Walk component tree. * * @param parent the parent * @param actionOnComponent the action to be performed on each component within the component tree */ public static void walkComponentTree(ParentComponent parent, Consumer<Component> actionOnComponent) { parent.components().forEachRemaining(component -> { if (component instanceof ParentComponent) { actionOnComponent.accept(component); walkComponentTree((ParentComponent) component, actionOnComponent); } else { actionOnComponent.accept(component); } }); } /** * Get the default image for the {@link Resource} editor. * * @return default image for the Resource Editor */ public FactoryResource getDefaultResourceImage() { return _classPathResourceLibraryHelper.createResource("no-image-placeholder.png"); } /** * Get the default User image for the {@link User} model and UIs. * * @return default User image Resource */ public FactoryResource getDefaultUserImage() { return _classPathResourceLibraryHelper.createResource("default-profile-picture.png"); } /** * Get the Role for Front End Access to the application * * @return the front end access role */ public Role getFrontEndAccessRole() { String frontEndRoleProgId = Preferences.systemRoot().get(PREF_KEY_FRONTEND_ROLE, null); if (StringFactory.isEmptyString(frontEndRoleProgId)) throw new IllegalStateException( "Frontend Role Programmatic Id should be defined in Preferences.systemRoot " + "with the key: " + PREF_KEY_FRONTEND_ROLE); return ofNullable(_roleDAO.getRoleByProgrammaticName(frontEndRoleProgId)) .orElseThrow(() -> new IllegalStateException( "Front End Role could not be foundfor programmatic id: " + frontEndRoleProgId)); } /** * Get the Role for Admin Access to the application * * @return the admin access role */ public Role getAdminAccessRole() { String adminRoleProgId = Preferences.systemRoot().get(PREF_KEY_ADMIN_ROLE, null); if (StringFactory.isEmptyString(adminRoleProgId)) throw new IllegalStateException( "Admin Role Programmatic Id should be defined in Preferences.systemRoot " + "with the key: " + PREF_KEY_ADMIN_ROLE); return ofNullable(_roleDAO.getRoleByProgrammaticName(adminRoleProgId)) .orElseThrow(() -> new IllegalStateException( "Admin Role could not be found for programmatic id: " + adminRoleProgId)); } /** * User has admin role boolean. * * @param user the user * * @return the boolean */ public boolean userHasAdminRole(User user) { return _principalDAO.getAllRoles(_entityRetriever.reattachIfNecessary(user).getPrincipal()) .contains(getAdminAccessRole()); } /** * User has admin role boolean. * * @param user the user * * @return the boolean */ public boolean userHasAdminRole(Principal user) { return _principalDAO.getAllRoles(_entityRetriever.reattachIfNecessary(user)).contains(getAdminAccessRole()); } /** * Get a new or existing Token for the given Principal * * @param principal the Principal * @param suffix the token suffix * * @return token */ public TokenCredentials getTokenForPrincipal(Principal principal, String suffix) { final TokenCredentials token = _principalDAO.getTokenCredentials(principal, suffix); if (token.getExpireDate() == null) { token.setExpireDate(AppUtil.getNewTokenExpireTime()); _principalDAO.saveCredentials(token); } return token; } /** * Get an expire time for a new token based off the current date * * @return token expire time */ public static Date getNewTokenExpireTime() { return new Date(Instant.now().plus(Duration.ofHours(80)).toEpochMilli()); } private Object readResolve() throws ObjectStreamException { ApplicationContext context = ApplicationContextUtils.getInstance().getContext(); assert context != null; return context.getBean(AppUtil.class); } @Contract(pure = true) private Object writeReplace() throws ObjectStreamException { return this; } }