Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package se.citerus.dddsample.infrastructure.i18n;

import org.springframework.context.i18n.LocaleContext;
import org.springframework.context.i18n.SimpleLocaleContext;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.i18n.AbstractLocaleContextResolver;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;

public class QueryParamLocaleResolver extends AbstractLocaleContextResolver {
private List<Locale> supportedLocales;

@Override
public LocaleContext resolveLocaleContext(HttpServletRequest request) {
String[] locales = request.getParameterMap().get("lang");
String localeName;
if (locales != null && locales.length > 0) {
localeName = locales[0];
} else {
return new SimpleLocaleContext(getDefaultLocale());
}

Locale requestLocale = convertLocaleName(localeName);
List<Locale> supportedLocales = getSupportedLocales();
if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {
return new SimpleLocaleContext(requestLocale);
}

Locale supportedLocale = findSupportedLocale(request, supportedLocales);
if (supportedLocale != null) {
return new SimpleLocaleContext(supportedLocale);
}
return new SimpleLocaleContext(getDefaultLocale() != null ? getDefaultLocale() : requestLocale);
}

private Locale findSupportedLocale(HttpServletRequest request, List<Locale> supportedLocales) {
Enumeration<Locale> requestLocales = request.getLocales();
Locale languageMatch = null;
while (requestLocales.hasMoreElements()) {
Locale locale = requestLocales.nextElement();
if (supportedLocales.contains(locale)) {
if (languageMatch == null || languageMatch.getLanguage().equals(locale.getLanguage())) {
// Full match: language + country, possibly narrowed from earlier language-only match
return locale;
}
}
else if (languageMatch == null) {
// Let's try to find a language-only match as a fallback
for (Locale candidate : supportedLocales) {
if (!StringUtils.hasLength(candidate.getCountry()) &&
candidate.getLanguage().equals(locale.getLanguage())) {
languageMatch = candidate;
break;
}
}
}
}
return languageMatch;
}

private Locale convertLocaleName(String localeName) {
return localeName.contains("_") ? new Locale(localeName.split("_")[0], localeName.split("_")[1]) : new Locale(localeName);
}

@Override
public void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext) {
// intentionally left blank
}

public List<Locale> getSupportedLocales() {
return supportedLocales;
}

public void setSupportedLocales(List<Locale> supportedLocales) {
this.supportedLocales = supportedLocales;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@
import org.springframework.lang.Nullable;
import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.FixedLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import se.citerus.dddsample.application.ApplicationEvents;
import se.citerus.dddsample.application.BookingService;
import se.citerus.dddsample.domain.model.cargo.CargoRepository;
import se.citerus.dddsample.domain.model.handling.HandlingEventRepository;
import se.citerus.dddsample.domain.model.location.LocationRepository;
import se.citerus.dddsample.domain.model.voyage.VoyageRepository;
import se.citerus.dddsample.infrastructure.i18n.QueryParamLocaleResolver;
import se.citerus.dddsample.interfaces.booking.facade.BookingServiceFacade;
import se.citerus.dddsample.interfaces.booking.facade.internal.BookingServiceFacadeImpl;
import se.citerus.dddsample.interfaces.handling.file.UploadDirectoryScanner;
Expand All @@ -29,6 +31,7 @@

import java.io.File;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Locale;

@Configuration
Expand All @@ -48,14 +51,25 @@ public class InterfacesApplicationContext implements WebMvcConfigurer {
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}

@Bean
public FixedLocaleResolver localeResolver() {
FixedLocaleResolver fixedLocaleResolver = new FixedLocaleResolver();
fixedLocaleResolver.setDefaultLocale(Locale.ENGLISH);
return fixedLocaleResolver;
public LocaleResolver localeResolver() {
QueryParamLocaleResolver localeResolver = new QueryParamLocaleResolver();
localeResolver.setSupportedLocales(List.of(Locale.ENGLISH,
Locale.SIMPLIFIED_CHINESE,
new Locale("sv", "SE"))); // add new locales here when available
localeResolver.setDefaultLocale(Locale.ENGLISH);
return localeResolver;
}

@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName("lang");
return lci;
}

@Bean
Expand Down Expand Up @@ -85,6 +99,7 @@ public void addInterceptors(InterceptorRegistry registry) {
OpenEntityManagerInViewInterceptor openSessionInViewInterceptor = new OpenEntityManagerInViewInterceptor();
openSessionInViewInterceptor.setEntityManagerFactory(entityManager.getEntityManagerFactory());
registry.addWebRequestInterceptor(openSessionInViewInterceptor);
registry.addInterceptor(localeChangeInterceptor());
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.support.RequestContextUtils;
import se.citerus.dddsample.interfaces.booking.facade.BookingServiceFacade;
import se.citerus.dddsample.interfaces.booking.facade.dto.CargoRoutingDTO;
import se.citerus.dddsample.interfaces.booking.facade.dto.LegDTO;
Expand Down Expand Up @@ -40,9 +42,11 @@
public final class CargoAdminController {

private final BookingServiceFacade bookingServiceFacade;
private final MessageSource messageSource;

public CargoAdminController(BookingServiceFacade bookingServiceFacade) {
public CargoAdminController(BookingServiceFacade bookingServiceFacade, MessageSource messageSource) {
this.bookingServiceFacade = bookingServiceFacade;
this.messageSource = messageSource;
}

@InitBinder
Expand All @@ -54,7 +58,7 @@ private void initBinder(HttpServletRequest request, ServletRequestDataBinder bin
public String registration(HttpServletRequest request, HttpServletResponse response, Map<String, Object> model) throws Exception {
List<LocationDTO> dtoList = bookingServiceFacade.listShippingLocations();

List<String> unLocodeStrings = new ArrayList<String>();
List<String> unLocodeStrings = new ArrayList<>();

for (LocationDTO dto : dtoList) {
unLocodeStrings.add(dto.getUnLocode());
Expand Down Expand Up @@ -94,6 +98,7 @@ public String show(HttpServletRequest request, HttpServletResponse response, Map

@RequestMapping("/selectItinerary")
public String selectItinerary(HttpServletRequest request, HttpServletResponse response, Map<String, Object> model) throws Exception {
model.put("viewAdapter", new CargoAdminViewAdapter(messageSource, RequestContextUtils.getLocale(request)));
String trackingId = request.getParameter("trackingId");

List<RouteCandidateDTO> routeCandidates = bookingServiceFacade.requestPossibleRoutesForCargo(trackingId);
Expand All @@ -107,7 +112,7 @@ public String selectItinerary(HttpServletRequest request, HttpServletResponse re

@RequestMapping(value = "/assignItinerary", method = RequestMethod.POST)
public void assignItinerary(HttpServletRequest request, HttpServletResponse response, RouteAssignmentCommand command) throws Exception {
List<LegDTO> legDTOs = new ArrayList<LegDTO>(command.getLegs().size());
List<LegDTO> legDTOs = new ArrayList<>(command.getLegs().size());
for (RouteAssignmentCommand.LegCommand leg : command.getLegs()) {
legDTOs.add(new LegDTO(
leg.getVoyageNumber(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package se.citerus.dddsample.interfaces.booking.web;

import org.springframework.context.MessageSource;
import se.citerus.dddsample.interfaces.booking.facade.dto.CargoRoutingDTO;

import java.util.Locale;

public class CargoAdminViewAdapter {
private final MessageSource messageSource;
private final Locale locale;

public CargoAdminViewAdapter(MessageSource messageSource, Locale locale) {
this.messageSource = messageSource;
this.locale = locale;
}

public String getSelectItinerarySummaryText(CargoRoutingDTO cargo) {
return messageSource.getMessage("cargo.admin.itinerary.summary", new Object[]{cargo.getTrackingId(), cargo.getOrigin(), cargo.getFinalDestination()}, locale);
}

public String getRouteCandidateCaption(Integer index) {
return messageSource.getMessage("cargo.admin.itinerary.routecandidatecaption", new Object[]{index}, locale);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
* helps us shield the domain model classes.
* <p>
*
* @see CargoTrackingViewAdapter
* @see se.citerus.dddsample.interfaces.tracking.CargoTrackingViewAdapter
* @see se.citerus.dddsample.interfaces.booking.web.CargoAdminController
*/
@Controller
Expand Down Expand Up @@ -63,17 +63,21 @@ private String onSubmit(final HttpServletRequest request,
final TrackCommand command,
final Map<String, Object> model,
final BindingResult bindingResult) {
final Locale locale = RequestContextUtils.getLocale(request);
trackCommandValidator.validate(command, bindingResult);
if (bindingResult.hasErrors()) {
bindingResult.rejectValue("trackingId", "error.required");
return "track";
}

final TrackingId trackingId = new TrackingId(command.getTrackingId());
final Cargo cargo = cargoRepository.find(trackingId);

if (cargo != null) {
final Locale locale = RequestContextUtils.getLocale(request);
final List<HandlingEvent> handlingEvents = handlingEventRepository.lookupHandlingHistoryOfCargo(trackingId).distinctEventsByCompletionTime();
model.put("cargo", new CargoTrackingViewAdapter(cargo, messageSource, locale, handlingEvents));
} else {
bindingResult.rejectValue("trackingId", "cargo.unknown_id", new Object[]{command.getTrackingId()}, "Unknown tracking id");
bindingResult.rejectValue("trackingId", "cargo.unknown_id", new Object[]{command.getTrackingId()}, "");
}
return "track";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public final class CargoTrackingViewAdapter {
private final String FORMAT = "yyyy-MM-dd hh:mm";
private final TimeZone timeZone;

/**
/**
* Constructor.
*
* @param cargo
Expand Down Expand Up @@ -97,6 +97,14 @@ public String getStatusText() {
return messageSource.getMessage(code, args, "[Unknown status]", locale);
}

public String getEtaText() {
return messageSource.getMessage("cargo.routing.eta", new Object[]{getDestination(), getEta()}, locale);
}

public String getStatusDescriptionText() {
return messageSource.getMessage("cargo.routing.result", new Object[]{getTrackingId(), getStatusText()}, locale);
}

/**
* @return Cargo destination location.
*/
Expand Down Expand Up @@ -131,19 +139,20 @@ public String getNextExpectedActivity() {
return "";
}

String text = "Next expected activity is to ";
HandlingEvent.Type type = activity.type();
if (type.sameValueAs(HandlingEvent.Type.LOAD)) {
return
text + type.name().toLowerCase() + " cargo onto voyage " + activity.voyage().voyageNumber() +
" in " + activity.location().name();
} else if (type.sameValueAs(HandlingEvent.Type.UNLOAD)) {
return
text + type.name().toLowerCase() + " cargo off of " + activity.voyage().voyageNumber() +
" in " + activity.location().name();
} else {
return text + type.name().toLowerCase() + " cargo in " + activity.location().name();
}
return messageSource.getMessage("cargo.routing.nextexpact." + type.name(), new Object[]{activity.voyage().voyageNumber(), activity.location().name()}, "Missing translation string", locale);
} else if (type.sameValueAs(HandlingEvent.Type.UNLOAD)) {
return messageSource.getMessage("cargo.routing.nextexpact." + type.name(), new Object[]{activity.voyage().voyageNumber(), activity.location().name()}, "Missing translation string", locale);
} else if (type.sameValueAs(HandlingEvent.Type.RECEIVE)) {
return messageSource.getMessage("cargo.routing.nextexpact." + type.name(), new Object[]{activity.location().name()}, "Missing translation string", locale);
} else if (type.sameValueAs(HandlingEvent.Type.CLAIM)) {
return messageSource.getMessage("cargo.routing.nextexpact." + type.name(), new Object[]{activity.location().name()}, "Missing translation string", locale);
} else if (type.sameValueAs(HandlingEvent.Type.CUSTOMS)) {
return messageSource.getMessage("cargo.routing.nextexpact." + type.name(), new Object[]{activity.location().name()}, "Missing translation string", locale);
} else {
return messageSource.getMessage("cargo.routing.nextexpact.unknown", null, "Missing translation string", locale);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,5 @@ public boolean supports(@NonNull final Class<?> clazz) {
public void validate(@NonNull final Object object,@NonNull final Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "trackingId", "error.required", "Required");
}

}

63 changes: 62 additions & 1 deletion src/main/resources/messages_en.properties
Original file line number Diff line number Diff line change
@@ -1,13 +1,74 @@
# this is the default language/locale
cargo.status.NOT_RECEIVED=Not received
cargo.status.IN_PORT=In port {0}
cargo.status.ONBOARD_CARRIER=Onboard voyage {0}
cargo.status.CLAIMED=Claimed
cargo.status.UNKNOWN=Unknown

cargo.misrouted=Misrouted
cargo.notrouted=Not routed

deliveryHistory.eventDescription.NOT_RECEIVED=Cargo has not yet been received.

deliveryHistory.eventDescription.LOAD=Loaded onto voyage {0} in {1}, at {2}.
deliveryHistory.eventDescription.UNLOAD=Unloaded off voyage {0} in {1}, at {2}.
deliveryHistory.eventDescription.RECEIVE=Received in {0}, at {1}.
deliveryHistory.eventDescription.CLAIM=Claimed in {0}, at {1}.
deliveryHistory.eventDescription.CUSTOMS=Cleared customs in {0}, at {1}.
deliveryHistory.eventDescription.CUSTOMS=Cleared customs in {0}, at {1}.

booking.header=Cargo Booking and Routing
booking.listall=List all cargos
booking.booknew=Book new cargo
cargo.tracking.header=Tracking cargo
cargo.tracking.footer=This application is written by
cargo.tracking.idinput=Enter your tracking id:
cargo.tracking.submitbutton=Track!
cargo.tracking.hint=Hint: try tracking "ABC123" or "JKL567".
cargo.tracking.handlinghistory=Handling History
cargo.tracking.misdirected=Cargo is misdirected
cargo.routing.result=Cargo {0} is now: {1}
cargo.routing.eta=Estimated time of arrival in {0}: {1}
cargo.routing.nextexpact.LOAD=Next expected activity is to load cargo onto voyage {0} in {1}
cargo.routing.nextexpact.UNLOAD=Next expected activity is to unload cargo off of voyage {0} in {1}
cargo.routing.nextexpact.RECEIVE=Next expected activity is to receive cargo in {0}
cargo.routing.nextexpact.CLAIM=Next expected activity is to claim cargo in {0}
cargo.routing.nextexpact.CUSTOMS=Next expected activity is to customs cargo in {0}
cargo.routing.nextexpact.unknown=Next expected activity is unknown

cargo.admin.header=Cargo Administration
cargo.admin.tablecaption=All cargos
cargo.admin.tableheader.trackingid=Tracking ID
cargo.admin.tableheader.origin=Origin
cargo.admin.tableheader.destination=Destination
cargo.admin.tableheader.routed=Routed
cargo.admin.tableheader.arrivaldeadline=Arrival deadline:
cargo.admin.details.caption=Details for cargo
cargo.admin.details.picknewdest=Change destination
cargo.admin.details.arrivaldeadline=Arrival deadline
cargo.admin.details.selectitinerary=Route this cargo
cargo.admin.details.reroute=reroute this cargo
cargo.admin.details.misrouted=Cargo is misrouted
cargo.admin.details.itinerary=Itinerary
cargo.admin.details.voyageno=Voyage number
cargo.admin.details.load=Load
cargo.admin.details.unload=Unload
cargo.admin.newdest.change=Change destination for cargo
cargo.admin.newdest.currentdest=Current destination
cargo.admin.newdest.newdest=New destination
cargo.admin.newdest.submit=Change destination
cargo.admin.register.caption=Book new cargo
cargo.admin.register.book=Book
cargo.admin.itinerary.caption=Select route
cargo.admin.itinerary.summary=Cargo {0} is going from {1} to {2}
cargo.admin.itinerary.noroutecandidates=No routes found that satisfy the route specification. Try setting an arrival deadline further into the future (a few weeks at least).
cargo.admin.itinerary.routecandidatecaption=Route candidate {0}
cargo.admin.itinerary.voyage=Voyage
cargo.admin.itinerary.submit=Assign cargo to this route

misc.yes=Yes
misc.no=No
misc.from=From
misc.to=To

error.required=The tracking id must not be empty
cargo.unknown_id=Unknown tracking id
Loading
Loading