Skip to content

Commit a41d7ed

Browse files
committed
refactor: Add http error 500 to resources
1 parent c94a697 commit a41d7ed

File tree

10 files changed

+324
-6
lines changed

10 files changed

+324
-6
lines changed

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ digitalpetri-modbus-tcp = "2.1.0"
1313
dropwizard-metrics = "4.2.33"
1414
equalsverifier = "3.17.5"
1515
errorprone = "2.38.0"
16+
freemarker = "2.3.34"
1617
future-converter = "1.2.0"
1718
guava = "33.4.8-jre"
1819
hikari = "6.2.1"
@@ -89,6 +90,7 @@ dropwizard-metrics-jvm = { module = "io.dropwizard.metrics:metrics-jvm", version
8990
dropwizard-metrics-logback = { module = "io.dropwizard.metrics:metrics-logback", version.ref = "dropwizard-metrics" }
9091
equalsverifier = { module = "nl.jqno.equalsverifier:equalsverifier", version.ref = "equalsverifier" }
9192
errorprone = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone" }
93+
freemarker = { module = "org.freemarker:freemarker", version.ref = "freemarker" }
9294
json-schema-inferrer = { module = "com.github.saasquatch:json-schema-inferrer", version.ref = "json-schema-inferrer" }
9395
jersey-container-jdk-http = { module = "org.glassfish.jersey.containers:jersey-container-jdk-http", version.ref = "jersey" }
9496
jersey-hk2 = { module = "org.glassfish.jersey.inject:jersey-hk2", version.ref = "jersey" }

hivemq-edge/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ dependencies {
194194
implementation(libs.victools.jsonschema.jackson)
195195
implementation(libs.json.schema.inferrer)
196196

197+
// i18n
198+
implementation(libs.freemarker)
199+
197200
// Edge modules
198201
compileOnly("com.hivemq:hivemq-edge-module-etherip")
199202
compileOnly("com.hivemq:hivemq-edge-module-plc4x")

hivemq-edge/src/main/java/com/hivemq/api/errors/HttpErrorFactory.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,19 @@
1616

1717
package com.hivemq.api.errors;
1818

19+
import com.hivemq.common.i18n.OpenAPIHttpError;
20+
import com.hivemq.edge.api.model.*;
1921
import com.hivemq.edge.api.model.InsufficientStorageError;
2022
import com.hivemq.edge.api.model.InternalServerError;
2123
import com.hivemq.edge.api.model.InvalidQueryParameterError;
22-
import com.hivemq.edge.api.model.PreconditionFailedError;
23-
import com.hivemq.edge.api.model.RequestBodyMissingError;
24-
import com.hivemq.edge.api.model.RequestBodyParameterMissingError;
2524
import com.hivemq.edge.api.model.TemporaryNotAvailableError;
2625
import com.hivemq.edge.api.model.UrlParameterMissingError;
2726
import com.hivemq.http.HttpStatus;
2827
import org.jetbrains.annotations.NotNull;
2928
import org.jetbrains.annotations.Nullable;
3029

30+
import java.util.Map;
31+
3132
public final class HttpErrorFactory extends ErrorFactory {
3233
private HttpErrorFactory() {
3334
super();
@@ -53,10 +54,10 @@ private HttpErrorFactory() {
5354
public static @NotNull InternalServerError internalServerError(final @Nullable String reason) {
5455
return InternalServerError.builder()
5556
.type(type(InternalServerError.class))
56-
.title("Internal Server Error")
57+
.title(OpenAPIHttpError.HTTP_ERROR_500_TITLE.get())
5758
.detail(reason == null ?
58-
"An unexpected error occurred, check the logs." :
59-
"An unexpected error occurred: " + reason)
59+
OpenAPIHttpError.HTTP_ERROR_500_DETAIL_DEFAULT.get() :
60+
OpenAPIHttpError.HTTP_ERROR_500_DETAIL_WITH_REASON.get(Map.of("reason", reason)))
6061
.status(HttpStatus.INTERNAL_SERVER_ERROR_500)
6162
.build();
6263
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2019-present HiveMQ GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.hivemq.common.i18n;
18+
19+
import org.jetbrains.annotations.NotNull;
20+
21+
public interface I18nTemplate {
22+
@NotNull String getKey();
23+
24+
@NotNull String getName();
25+
26+
@NotNull String getResourceName();
27+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2019-present HiveMQ GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.hivemq.common.i18n;
18+
19+
import org.jetbrains.annotations.NotNull;
20+
import org.jetbrains.annotations.Nullable;
21+
22+
import java.util.Locale;
23+
24+
public final class LocaleContext {
25+
public static @NotNull Locale DEFAULT_LOCALE = Locale.US;
26+
27+
private static final ThreadLocal<Locale> THREAD_LOCAL_LOCALE = ThreadLocal.withInitial(() -> DEFAULT_LOCALE);
28+
29+
public static @NotNull Locale getCurrentLocale() {
30+
return THREAD_LOCAL_LOCALE.get();
31+
}
32+
33+
public static void setCurrentLocale(final @Nullable Locale locale) {
34+
THREAD_LOCAL_LOCALE.set(locale == null ? DEFAULT_LOCALE : locale);
35+
}
36+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2019-present HiveMQ GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.hivemq.common.i18n;
18+
19+
import freemarker.cache.StringTemplateLoader;
20+
import freemarker.template.Configuration;
21+
import freemarker.template.Template;
22+
import freemarker.template.TemplateException;
23+
import org.apache.commons.io.IOUtils;
24+
import org.jetbrains.annotations.NotNull;
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
28+
import java.io.IOException;
29+
import java.io.StringReader;
30+
import java.io.StringWriter;
31+
import java.nio.charset.StandardCharsets;
32+
import java.util.Locale;
33+
import java.util.Map;
34+
import java.util.Properties;
35+
import java.util.concurrent.ConcurrentHashMap;
36+
37+
/**
38+
* Singleton class to manage OpenAPI error templates using FreeMarker.
39+
* It provides methods to retrieve error messages based on the current locale and template keys.
40+
* <p>
41+
* This class is thread-safe and uses a cache to store configurations for different locales.
42+
*/
43+
public final class OpenAPIErrorTemplate {
44+
45+
private static final OpenAPIErrorTemplate INSTANCE = new OpenAPIErrorTemplate();
46+
47+
private final @NotNull Map<String, Configuration> configurationMap;
48+
private final @NotNull Logger logger;
49+
50+
private OpenAPIErrorTemplate() {
51+
configurationMap = new ConcurrentHashMap<>();
52+
logger = LoggerFactory.getLogger(getClass());
53+
}
54+
55+
public static @NotNull OpenAPIErrorTemplate getInstance() {
56+
return INSTANCE;
57+
}
58+
59+
private Configuration createConfiguration(final @NotNull Locale locale) {
60+
final Configuration configuration = new Configuration(Configuration.VERSION_2_3_34);
61+
configuration.setDefaultEncoding(StandardCharsets.UTF_8.name());
62+
final StringTemplateLoader stringTemplateLoader = new StringTemplateLoader();
63+
configuration.setTemplateLoader(stringTemplateLoader);
64+
configuration.setLocale(locale);
65+
return configuration;
66+
}
67+
68+
public @NotNull String get(final @NotNull I18nTemplate i18nTemplate) {
69+
return get(i18nTemplate, Map.of());
70+
}
71+
72+
public @NotNull String get(final @NotNull I18nTemplate i18nTemplate, final @NotNull Map<String, Object> map) {
73+
final Locale locale = LocaleContext.getCurrentLocale();
74+
Configuration configuration = configurationMap.get(locale.toString());
75+
if (configuration == null) {
76+
configuration = createConfiguration(locale);
77+
configurationMap.put(locale.toString(), configuration);
78+
}
79+
try {
80+
final StringTemplateLoader stringTemplateLoader = (StringTemplateLoader) configuration.getTemplateLoader();
81+
if (stringTemplateLoader.findTemplateSource(i18nTemplate.getKey()) == null) {
82+
final Properties properties = new Properties();
83+
try (final StringReader stringReader = new StringReader(IOUtils.resourceToString(i18nTemplate.getResourceName(),
84+
StandardCharsets.UTF_8))) {
85+
properties.load(stringReader);
86+
}
87+
synchronized (configuration) {
88+
for (final Map.Entry<Object, Object> entry : properties.entrySet()) {
89+
stringTemplateLoader.putTemplate(String.valueOf(entry.getKey()),
90+
String.valueOf(entry.getValue()));
91+
}
92+
}
93+
}
94+
final Template template = configuration.getTemplate(i18nTemplate.getKey());
95+
try (final StringWriter stringWriter = new StringWriter()) {
96+
template.process(map, stringWriter);
97+
return stringWriter.toString();
98+
}
99+
} catch (final TemplateException e) {
100+
final String errorMessage =
101+
"Error: Template " + i18nTemplate.getKey() + " for " + locale + " could not be processed.";
102+
logger.error(errorMessage);
103+
return errorMessage;
104+
} catch (final IOException e) {
105+
final String errorMessage =
106+
"Error: Template " + i18nTemplate.getKey() + " for " + locale + " could not be loaded.";
107+
logger.error(errorMessage);
108+
return errorMessage;
109+
}
110+
}
111+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2019-present HiveMQ GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.hivemq.common.i18n;
18+
19+
import org.jetbrains.annotations.NotNull;
20+
21+
import java.util.Map;
22+
23+
public enum OpenAPIHttpError implements I18nTemplate {
24+
HTTP_ERROR_500_TITLE("http.error.500.title"),
25+
HTTP_ERROR_500_DETAIL_DEFAULT("http.error.500.detail.default"),
26+
HTTP_ERROR_500_DETAIL_WITH_REASON("http.error.500.detail.with.reason"),
27+
;
28+
29+
private static final String RESOURCE_NAME_PREFIX = "/templates/openapi-errors-";
30+
private static final String RESOURCE_NAME_SUFFIX = ".properties";
31+
private final @NotNull String key;
32+
33+
OpenAPIHttpError(final @NotNull String key) {
34+
this.key = key;
35+
}
36+
37+
public @NotNull String get() {
38+
return get(Map.of());
39+
}
40+
41+
public @NotNull String get(final @NotNull Map<String, Object> map) {
42+
return OpenAPIErrorTemplate.getInstance().get(this, map);
43+
}
44+
45+
@Override
46+
public @NotNull String getKey() {
47+
return key;
48+
}
49+
50+
@Override
51+
public @NotNull String getName() {
52+
return name();
53+
}
54+
55+
@Override
56+
public @NotNull String getResourceName() {
57+
return RESOURCE_NAME_PREFIX + LocaleContext.getCurrentLocale() + RESOURCE_NAME_SUFFIX;
58+
}
59+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
http.error.500.title=Internal Server Error
2+
http.error.500.detail.default=An unexpected error occurred, check the logs.
3+
http.error.500.detail.with.reason=An unexpected error occurred: ${reason}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2019-present HiveMQ GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.hivemq.common.i18n;
18+
19+
import org.junit.Test;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
23+
public class OpenAPIErrorTemplateTest {
24+
@Test
25+
public void whenLocaleIsNull_thenUseDefaultLocale() {
26+
LocaleContext.setCurrentLocale(null);
27+
assertThat(LocaleContext.getCurrentLocale()).isEqualTo(LocaleContext.DEFAULT_LOCALE);
28+
}
29+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2019-present HiveMQ GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.hivemq.common.i18n;
18+
19+
import org.junit.Test;
20+
21+
import java.util.Locale;
22+
import java.util.Map;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
26+
public class OpenAPIHttpErrorTest {
27+
@Test
28+
public void whenLocaleIsEnUS_thenHttpError500ShouldWork() {
29+
LocaleContext.setCurrentLocale(Locale.US);
30+
assertThat(OpenAPIHttpError.HTTP_ERROR_500_TITLE.get()).isEqualTo("Internal Server Error");
31+
assertThat(OpenAPIHttpError.HTTP_ERROR_500_DETAIL_DEFAULT.get()).isEqualTo(
32+
"An unexpected error occurred, check the logs.");
33+
assertThat(OpenAPIHttpError.HTTP_ERROR_500_DETAIL_WITH_REASON.get(Map.of("reason", "test."))).isEqualTo(
34+
"An unexpected error occurred: test.");
35+
}
36+
37+
@Test
38+
public void whenLocaleIsEnGB_thenHttpError500ShouldFail() {
39+
LocaleContext.setCurrentLocale(Locale.UK);
40+
assertThat(OpenAPIHttpError.HTTP_ERROR_500_TITLE.get()).isEqualTo(
41+
"Error: Template http.error.500.title for en_GB could not be loaded.");
42+
assertThat(OpenAPIHttpError.HTTP_ERROR_500_DETAIL_DEFAULT.get()).isEqualTo(
43+
"Error: Template http.error.500.detail.default for en_GB could not be loaded.");
44+
assertThat(OpenAPIHttpError.HTTP_ERROR_500_DETAIL_WITH_REASON.get(Map.of("reason", "test."))).isEqualTo(
45+
"Error: Template http.error.500.detail.with.reason for en_GB could not be loaded.");
46+
}
47+
}

0 commit comments

Comments
 (0)