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
Expand Up @@ -3,13 +3,14 @@
import com.dotcms.health.util.HealthCheckBase;
import com.dotcms.rendering.velocity.util.VelocityUtil;
import com.dotmarketing.util.Logger;

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;

import javax.enterprise.context.ApplicationScoped;
import java.io.StringWriter;

import static com.dotmarketing.util.WebKeys.DOTCMS_STARTED_UP;

/**
* CDI-based health check that verifies the Velocity engine's global macro
* library is loaded and resolvable.
Expand Down Expand Up @@ -78,6 +79,14 @@ protected CheckResult performCheck() throws Exception {
"Velocity health check skipped during shutdown");
}

// VelocityUtil.getEngine() triggers a lazy init that touches the DB/company context.
// On a fresh install this fires before Task00001LoadSchema runs, throwing "No Company!".
// Defer until InitServlet has set dotcms.started.up=true.
Comment thread
jcastro-dotcms marked this conversation as resolved.
if (!"true".equals(System.getProperty(DOTCMS_STARTED_UP))) {
return new CheckResult(false, 0L,
"Velocity probe deferred: dotCMS startup not yet complete");
}

return measureExecution(() -> {
final VelocityEngine engine = VelocityUtil.getEngine();
final StringWriter writer = new StringWriter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,22 @@ private static Map<String, Object> getMapForLanguage(final String id) {

final Language language = APILocator.getLanguageAPI().getLanguage(id);

return new HashMap<>(UtilMethods.isSet(language) ?
Map.of(
TITLE_KEY, String.format( "%s(%s)", language.getLanguage(), language.getCountryCode()),
LANGUAGE_CODE_KEY, language.getLanguageCode(),
COUNTRY_CODE_KEY, language.getCountryCode(),
CONTENT_TYPE_NAME_KEY, CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL,
PusheableAsset.LANGUAGE.getType())
) :
Map.of(TITLE_KEY, id));
if (!UtilMethods.isSet(language)) {
return new HashMap<>(Map.of(TITLE_KEY, id));
}

// language-only locales (e.g. "es", no country) have a null countryCode; Map.of()
// throws NullPointerException on a null value, so default it to blank.
final String countryCode = UtilMethods.isSet(language.getCountryCode())
? language.getCountryCode() : StringPool.BLANK;

return new HashMap<>(Map.of(
TITLE_KEY, String.format("%s(%s)", language.getLanguage(), countryCode),
LANGUAGE_CODE_KEY, language.getLanguageCode(),
COUNTRY_CODE_KEY, countryCode,
CONTENT_TYPE_NAME_KEY, CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL,
PusheableAsset.LANGUAGE.getType())
));
}

private static Map<String, Object> getMapForCategory(){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ private Visitor createVisitor(final HttpServletRequest request) {
}

final Language selectedLanguage = languageWebAPI.getLanguage(request);
final Locale locale = new Locale(selectedLanguage.getLanguageCode(), selectedLanguage.getCountryCode());
final Locale locale = selectedLanguage.asLocale();
final UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
final UUID dmid = lookupDMID(request);
final boolean isNewVisitor = isNewVisitor(request);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
package com.dotmarketing.portlets.contentlet.transform.strategy;

import static com.dotmarketing.portlets.contentlet.transform.strategy.TransformOptions.AVOID_MAP_SUFFIX_FOR_VIEWS;
import static com.liferay.portal.language.LanguageUtil.getLiteralLocale;

import com.dotcms.api.APIProvider;
import com.dotcms.util.CollectionsUtils;
import com.dotmarketing.portlets.contentlet.model.Contentlet;
import com.dotmarketing.portlets.languagesmanager.model.Language;
import com.dotmarketing.util.UtilMethods;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.liferay.portal.model.User;
import com.liferay.util.StringPool;

import java.util.Collections;
import java.util.Map;
import java.util.Set;

import static com.dotmarketing.portlets.contentlet.transform.strategy.TransformOptions.AVOID_MAP_SUFFIX_FOR_VIEWS;
import static com.liferay.portal.language.LanguageUtil.getLiteralLocale;
import static com.liferay.util.StringPool.BLANK;

/**
* Language view transformer strategy
* Language view transformer strategy.
* <p>
* Maps a Contentlet's {@link Language} into the response Map used by the various content
* transformer presets (e.g. {@code webAssetOptions}, {@code dotAssetOptions}) as well as
* {@link HistoryViewStrategy}. Locales are allowed to be language-only (no country code), so
* the {@code country}/{@code countryCode} entries default to an empty string rather than
* {@code null} to avoid tripping Guava's {@link Builder}, which rejects null values.
*
* @author Fabrizzio Araya
* @since Jun 11th, 2020
*/
public class LanguageViewStrategy extends AbstractTransformStrategy<Contentlet>{

Expand Down Expand Up @@ -50,10 +62,18 @@ public static Map<String, Object> mapLanguage(final Language language, final boo
}

/**
* Lang functions now relocated here.
* @param language
* @param wrapAsMap
* @return
* Maps a {@link Language} into a Map of view-friendly properties (id, language/country names
* and codes, ISO code, flag). Since a Language may be defined with only a language code and
* no country (e.g. {@code es}), {@code country} and {@code countryCode} default to an empty
* string when unset rather than {@code null} — a null value would blow up the underlying
* {@link Builder}, which does not accept null entries.
*
* @param language the Language to map; its {@code country}/{@code countryCode} may be null
* @param wrapAsMap if {@code true}, wraps the result under a {@code language}/{@code languageMap}
* key (see {@code AVOID_MAP_SUFFIX_FOR_VIEWS}); if {@code false}, returns the
* properties unwrapped
* @param options the active {@link TransformOptions}, used to decide the wrapper key suffix
* @return a Map of the language's view properties
*/
public static Map<String, Object> mapLanguage(final Language language, final boolean wrapAsMap,
final Set<TransformOptions> options) {
Expand All @@ -64,8 +84,8 @@ public static Map<String, Object> mapLanguage(final Language language, final boo
.put("languageId", language.getId())
.put("language", language.getLanguage())
.put("languageCode", language.getLanguageCode())
.put("country", language.getCountry())
.put("countryCode", language.getCountryCode())
.put("country", CollectionsUtils.orElseGet(language.getCountry(), BLANK))
.put("countryCode", CollectionsUtils.orElseGet(language.getCountryCode(), BLANK))
.put("languageFlag", getLiteralLocale(language.getLanguageCode(), language.getCountryCode()));

final String iso = UtilMethods.isSet(language.getCountryCode())
Expand All @@ -75,9 +95,9 @@ public static Map<String, Object> mapLanguage(final Language language, final boo

if(wrapAsMap){
builder.put("id", language.getId());
final String sufix = options.contains(AVOID_MAP_SUFFIX_FOR_VIEWS)
final String suffix = options.contains(AVOID_MAP_SUFFIX_FOR_VIEWS)
? "" : "Map";
return ImmutableMap.of("language" + sufix, builder.build());
return ImmutableMap.of("language" + suffix, builder.build());
}
return builder.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@

import java.io.Writer;

import static com.dotmarketing.util.WebKeys.DOTCMS_STARTED_UP;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.*;

/**
* Unit tests for {@link VelocityHealthCheck}. Exercises the probe via a mocked
Expand All @@ -28,15 +27,26 @@
public class VelocityHealthCheckTest {

private MockedStatic<VelocityUtil> velocityUtilMock;
private String previousStartedUpProperty;

@Before
public void setUp() {
// The probe defers until dotCMS startup completes, which InitServlet signals via this
// property in production. Set it here so the probe logic under test actually runs
// instead of short-circuiting on every test.
previousStartedUpProperty = System.getProperty(DOTCMS_STARTED_UP);
System.setProperty(DOTCMS_STARTED_UP, "true");
velocityUtilMock = mockStatic(VelocityUtil.class);
}

@After
public void tearDown() {
velocityUtilMock.close();
if (previousStartedUpProperty == null) {
System.clearProperty(DOTCMS_STARTED_UP);
} else {
System.setProperty(DOTCMS_STARTED_UP, previousStartedUpProperty);
}
}

@Test
Expand All @@ -48,6 +58,18 @@ public void returnsUpWhenRenderMarksResolves() {
assertEquals(HealthStatus.UP, result.status());
}

@Test
public void returnsDownWhenStartupNotComplete() {
// Even a healthy-looking engine shouldn't matter: the probe must defer before touching
// VelocityUtil.getEngine() at all until InitServlet marks startup complete.
System.clearProperty(DOTCMS_STARTED_UP);
stubEngineToWrite("<span class=\"editor-marks\"></span>");

final HealthCheckResult result = new VelocityHealthCheck().check();

assertEquals(HealthStatus.DOWN, result.status());
}

@Test
public void returnsDownWhenRenderMarksRendersLiterally() {
// When the global macro library failed to load, Velocity renders the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.dotcms.publisher.business;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mockStatic;

import com.dotcms.publisher.util.PusheableAsset;
import com.dotmarketing.business.APILocator;
import com.dotmarketing.portlets.languagesmanager.business.LanguageAPI;
import com.dotmarketing.portlets.languagesmanager.model.Language;
import java.util.Map;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

/**
* Unit tests for {@link PublishQueueElementTransformer}.
*
* @author Jose Castro
* @since Jul 2, 2026
*/
public class PublishQueueElementTransformerTest {

private MockedStatic<APILocator> apiLocatorMock;
private LanguageAPI languageAPI;

@Before
public void setUp() {
languageAPI = Mockito.mock(LanguageAPI.class);
apiLocatorMock = mockStatic(APILocator.class);
apiLocatorMock.when(APILocator::getLanguageAPI).thenReturn(languageAPI);
}

@After
public void tearDown() {
apiLocatorMock.close();
}

/**
* Regression test for the Push Publishing queue view crashing on language-only locales.
* <p>
* {@code getMapForLanguage} used to build a {@code java.util.Map.of(...)} directly from
* {@code language.getCountryCode()}, which is legitimately null for a language-only locale
* (e.g. {@code es}, no country). {@code Map.of()} throws {@code NullPointerException} on any
* null value, the same failure mode as Guava's {@code ImmutableMap.Builder}.
*/
@Test
public void testGetMap_whenLanguageIsLanguageOnly_doesNotThrowAndDefaultsCountryToBlank() {
final Language language = new Language(1L, "es", null, "Spanish", null);
Mockito.when(languageAPI.getLanguage("1")).thenReturn(language);

final PublishQueueElementTransformer transformer = new PublishQueueElementTransformer();
final Map<String, Object> result = transformer.getMap("1", PusheableAsset.LANGUAGE.getType());

assertEquals("", result.get(PublishQueueElementTransformer.COUNTRY_CODE_KEY));
assertEquals("es", result.get(PublishQueueElementTransformer.LANGUAGE_CODE_KEY));
assertEquals("Spanish()", result.get(PublishQueueElementTransformer.TITLE_KEY));
}

/**
* Non-regression check: a locale with both language and country must keep mapping the
* country code through unchanged.
*/
@Test
public void testGetMap_whenLanguageHasCountry_mapsCountryCode() {
final Language language = new Language(2L, "es", "CR", "Spanish", "Costa Rica");
Mockito.when(languageAPI.getLanguage("2")).thenReturn(language);

final PublishQueueElementTransformer transformer = new PublishQueueElementTransformer();
final Map<String, Object> result = transformer.getMap("2", PusheableAsset.LANGUAGE.getType());

assertEquals("CR", result.get(PublishQueueElementTransformer.COUNTRY_CODE_KEY));
assertEquals("Spanish(CR)", result.get(PublishQueueElementTransformer.TITLE_KEY));
}
}
Loading
Loading