Skip to main content

Resolving 'Cannot Find sitemap.xml' Issue

Β· One min read
Haril Song
Owner, Software Engineer at 42dot

I had registered the sitemap.xml for my blog to ensure it gets indexed by Google, but all I was getting was an error message saying 'sitemap not found'. Finally, I managed to resolve it, and I am sharing the method I used.

While this method may not solve every case, it seems worth a try.

Simply run the following command:

curl https://www.google.com/ping\?sitemap\={path to your submitted sitemap}

And then, when you check the search console again...!

sitemap-success Finally resolved after almost a month...😒

The sitemap is finally being recognized.

Hope this helps!

Reference​

[Spring Batch] KafkaItemReader

Β· 2 min read
Haril Song
Owner, Software Engineer at 42dot
info

I used Docker to install Kafka before writing this post, but that content is not covered here.

What is KafkaItemReader..?​

In Spring Batch, the KafkaItemReader is provided for processing data from Kafka topics.

Let's create a simple batch job.

Example​

First, add the necessary dependencies.

dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-batch'
implementation 'org.springframework.kafka:spring-kafka'
...
}

Configure Kafka settings in application.yml.

spring:
kafka:
bootstrap-servers:
- localhost:9092
consumer:
group-id: batch
@Slf4j
@Configuration
@RequiredArgsConstructor
public class KafkaSubscribeJobConfig {

private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
private final KafkaProperties kafkaProperties;

@Bean
Job kafkaJob() {
return jobBuilderFactory.get("kafkaJob")
.incrementer(new RunIdIncrementer())
.start(step1())
.build();
}

@Bean
Step step1() {
return stepBuilderFactory.get("step1")
.<String, String>chunk(5)
.reader(kafkaItemReader())
.writer(items -> log.info("items: {}", items))
.build();
}

@Bean
KafkaItemReader<String, String> kafkaItemReader() {
Properties properties = new Properties();
properties.putAll(kafkaProperties.buildConsumerProperties());

return new KafkaItemReaderBuilder<String, String>()
.name("kafkaItemReader")
.topic("test") // 1.
.partitions(0) // 2.
.partitionOffsets(new HashMap<>()) // 3.
.consumerProperties(properties) // 4.
.build();
}
}
  1. Specify the topic from which to read the data.
  2. Specify the partition of the topic; multiple partitions can be specified.
  3. If no offset is specified in KafkaItemReader, it reads from offset 0. Providing an empty map reads from the last offset.
  4. Set the essential properties for execution.
tip

KafkaProperties provides various public interfaces to conveniently use Kafka in Spring.

Try it out​

Now, when you run the batch job, consumer groups are automatically created based on the information in application.yml, and the job starts subscribing to the topic.

Let's use the kafka console producer to add data from 1 to 10 to the test topic.

kafka-console-producer.sh --bootstrap-server localhost:9092 --topic test

produce-topic

You can see that the batch job is successfully subscribing to the topic.

subscribe-batch

Since we set the chunkSize to 5, the data is processed in batches of 5.

So far, we have looked at the basic usage of KafkaItemReader in Spring Batch. Next, let's see how to write test code.

Easily Perform Static Code Analysis with Qodana

Β· 2 min read
Haril Song
Owner, Software Engineer at 42dot

What is Qodana?​

Qodana is a code quality improvement tool provided by JetBrains. It is very easy to use, so I would like to introduce it briefly.

First, you need an environment with Docker installed.

docker run --rm -it -p 8080:8080 \
-v <source-directory>/:/data/project/ \
-v <output-directory>/:/data/results/ \
jetbrains/qodana-jvm --show-report

I am analyzing a Java application, so I used the jvm image. If you are using a different language, you can find the appropriate image on Qodana's website.

  • Replace <source-directory> with the path to the project you want to analyze.
  • Enter the path where the analysis results will be stored in <output-directory>. I will explain this further below.

To store the analysis results, I created a folder named qodana in the root directory.

mkdir ~/qodana
# Then replace <output-directory> with ~/qobana.

Now, execute the docker run ~ command written above and wait for a while to see the results as shown below.

I used a simple Java application for testing.

image

Now, if you access http://localhost:8080, you can see the code analysis results.

image1

If you have Docker installed, you can easily obtain the code analysis results of your current project.

Such analysis tools serve as a form of code review, reducing the reviewer's fatigue and allowing them to focus on more detailed reviews. Actively utilizing code quality management tools like this can lead to a very convenient development experience.

[Spring Batch] Implementing Custom Constraint Writer

Β· 4 min read
Haril Song
Owner, Software Engineer at 42dot

Situation πŸ§β€‹

Recently, I designed a batch process that uses Upsert in PostgreSQL for a specific logic. During implementation, due to a change in business requirements, I had to add a specific column to a composite unique condition.

The issue arose from the fact that the unique constraint of the composite unique column does not prevent duplicates with null values in a specific column.

Let's take a look at an example of the problematic situation.

create table student
(
id integer not null
constraint student_pk
primary key,
name varchar,
major varchar,
constraint student_unique
unique (name, major)
);
idnamemajor
1songkorean
2kimenglish
3parkmath
4kimNULL
5kimNULL

To avoid allowing null duplicates, the idea of inserting dummy data naturally came to mind, but I felt reluctant to store meaningless data in the database. Especially if the column where null occurs stores complex data like UUID, it would be very difficult to identify meaningless values buried among other values.

Although it may be a bit cumbersome, using a unique partial index allows us to disallow null values without inserting dummy data. I decided to pursue the most ideal solution, even if it is challenging.

Solution​

Partial Index​

CREATE UNIQUE INDEX stu_2col_uni_idx ON student (name, major)
WHERE major IS NOT NULL;

CREATE UNIQUE INDEX stu_1col_uni_idx ON student (name)
WHERE major IS NULL;

PostgreSQL provides the functionality of partial indexes.

Partial Index : A feature that creates an index only when certain conditions are met. It allows for efficient index creation and maintenance by narrowing the scope of the index.

When a value with only name is inserted, stu_1col_uni_idx allows only one row with the same name where major is null. By creating two complementary indexes, we can skillfully prevent duplicates with null values in a specific column.

duplicate error An error occurs when trying to store a value without major

However, when there are two unique constraints like this, since only one constraint check is allowed during Upsert execution, the batch did not run as intended.

After much deliberation, I decided to check if a specific value is missing before executing the SQL and then execute the SQL that meets the conditions.

Implementing SelectConstraintWriter​

public class SelectConstraintWriter extends JdbcBatchItemWriter<Student> {

@Setter
private String anotherSql;

@Override
public void write(List<? extends Student> items) {
if (items.isEmpty()) {
return;
}

List<? extends Student> existMajorStudents = items.stream()
.filter(student -> student.getMajor() != null)
.collect(toList());

List<? extends Student> nullMajorStudents = items.stream()
.filter(student -> student.getMajor() == null)
.collect(toList());

executeSql(existMajorStudents, sql);
executeSql(nullMajorStudents, anotherSql);
}

private void executeSql(List<? extends student> students, String sql) {
if (logger.isDebugEnabled()) {
logger.debug("Executing batch with " + students.size() + " items.");
}

int[] updateCounts;

if (usingNamedParameters) {
if (this.itemSqlParameterSourceProvider == null) {
updateCounts = namedParameterJdbcTemplate.batchUpdate(sql, students.toArray(new Map[students.size()]));
} else {
SqlParameterSource[] batchArgs = new SqlParameterSource[students.size()];
int i = 0;
for (student item : students) {
batchArgs[i++] = itemSqlParameterSourceProvider.createSqlParameterSource(item);
}
updateCounts = namedParameterJdbcTemplate.batchUpdate(sql, batchArgs);
}
} else {
updateCounts = namedParameterJdbcTemplate.getJdbcOperations().execute(sql,
(PreparedStatementCallback<int[]>) ps -> {
for (student item : students) {
itemPreparedStatementSetter.setValues(item, ps);
ps.addBatch();
}
return ps.executeBatch();
});
}

if (assertUpdates) {
for (int i = 0; i < updateCounts.length; i++) {
int value = updateCounts[i];
if (value == 0) {
throw new EmptyResultDataAccessException("Item " + i + " of " + updateCounts.length
+ " did not update any rows: [" + students.get(i) + "]", 1);
}
}
}
}
}

I implemented this by overriding the write method of the JdbcBatchItemWriter that was previously used. By checking the presence of major in the code and selecting and executing the appropriate SQL, we can ensure that the Upsert statement works correctly instead of encountering a duplicateKeyException.

Here is an example of usage:

@Bean
SelectConstraintWriter studentItemWriter() {
String sql1 =
"INSERT INTO student(id, name, major) "
+ "VALUES (nextval('hibernate_sequence'), :name, :major) "
+ "ON CONFLICT (name, major) WHERE major IS NOT NULL "
+ "DO UPDATE "
+ "SET name = :name, "
+ " major = :major";

String sql2 =
"INSERT INTO student(id, name, major) "
+ "VALUES (nextval('hibernate_sequence'), :name, :major) "
+ "ON CONFLICT (name) WHERE major IS NULL "
+ "DO UPDATE "
+ "SET name = :name, "
+ " major = :major";

SelectConstraintWriter writer = new SelectConstraintWriter();
writer.setSql(sql1);
writer.setAnotherSql(sql2);
writer.setDataSource(dataSource);
writer.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>());
writer.afterPropertiesSet();
return writer;
}

Conclusion​

It's regrettable that if PostgreSQL allowed multiple constraint checks during Upsert execution, we wouldn't have needed to go to such lengths. I hope for updates in future versions.


Reference​

create unique constraint with null columns

[Kotlin] Infix Functions

Β· One min read
Haril Song
Owner, Software Engineer at 42dot

In Kotlin, there is a method of defining functions called Infix functions, which is a syntax that was unimaginable while using Java as the primary language. Let's introduce this for those who are starting with Kotlin.

Member functions with a single parameter can be converted into Infix functions.

One of the prominent examples of Infix functions is the to function included in the standard library.

val pair = "Ferrari" to "Katrina"
println(pair)
// (Ferrari, Katrina)

You can define new Infix functions like to as needed. For example, you can extend Int as follows:

infix fun Int.times(str: String) = str.repeat(this)
println(2 times "Hello ")
// Hello Hello

If you want to redefine to as a new Infix function called onto, you can write it as follows:

infix fun String.onto(other: String) = Pair(this, other)
val myPair = "McLaren" onto "Lucas"
println(myPair)
// (McLaren, Lucas)

Such Kotlin syntax enables quite unconventional coding methods.

class Person(val name: String) {
val likedPeople = mutableListOf<Person>()

infix fun likes(other: Person) {
likedPeople.add(other)
}
}

fun main() {
val sophia = Person("Sophia")
val claudia = Person("Claudia")

sophia likes claudia // !!
}

Reference​

Kotlin Docs

[Kotlin] Enhanced Loops

Β· 2 min read
Haril Song
Owner, Software Engineer at 42dot

In Kotlin, you can write much simpler and more convenient loops compared to Java. Let's see how you can use them.

1. .. operator​

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in 0..fruits.size - 1) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

Using .. creates a traditional loop that increments by 1.

2. downTo​

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in fruits.size - 1 downTo 0) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

Using downTo creates a loop that decrements as expected.

3. step​

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in 0..fruits.size - 1 step 2) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

With the step keyword, you can implement a loop that skips a specific number of elements. This also applies to downTo.

4. until​

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in 0 until fruits.size) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

Using until creates a loop that does not include the last number, eliminating the need for -1.

5. lastIndex​

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in 0 .. fruits.lastIndex) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

Now, using the lastIndex property, loops start to become easier to read. But of course, there's more to explore.

6. indices​

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in fruits.indices) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

indices returns the index range of the collection.

7. withIndex()​

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for ((index, fruit) in fruits.withIndex()) {
println("$index: $fruit")
}
}

By using withIndex(), extracting both index and value simultaneously simplifies the code, resembling Python's simplicity. This should be sufficient for most loop scenarios, but there's one more method left.

8. forEachIndexed​

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
fruits.forEachIndexed { index, fruit ->
println("$index: $fruit")
}
}

Using a lambda function with forEachIndexed can make the code more concise, intuitive, and straightforward. Choose the appropriate method that suits your needs.


Reference​

Kotlin Tips: Loops

Utilizing Ellipsoids on Earth with Kotlin

Β· 3 min read
Haril Song
Owner, Software Engineer at 42dot

Background​

earth image reference1

Considering that the Earth is neither flat nor a perfect sphere, but an irregular ellipsoid, there is no perfect formula for quickly and accurately calculating the distance between two points at different longitudes and latitudes.

However, by using the geotools library, you can obtain mathematically corrected approximations quite easily.

Adding Dependencies​

To use the Earth ellipsoid in geotools, you need to add the relevant library dependencies.

repositories {
maven { url "https://repo.osgeo.org/repository/release/" }
maven { url "https://download.osgeo.org/webdav/geotools/" }
mavenCentral()
}

dependencies {
...
implementation 'org.geotools:gt-referencing:26.2'
...
}

Writing Code​

First, define the coordinates of Seoul and Busan as an enum class.

enum class City(val latitude: Double, val longitude: Double) {
SEOUL(37.5642135, 127.0016985),
BUSAN(35.1104, 129.0431);
}

Next, let's look at a simple usage example through a test code.

class EllipsoidTest {

@Test
internal fun createEllipsoid() {
val ellipsoid = DefaultEllipsoid.WGS84 // Creates an ellipsoid that is as close to the Earth as possible using the WGS84 geodetic system used in GPS

val isSphere = ellipsoid.isSphere // Determines if it is a sphere or an ellipsoid
val semiMajorAxis = ellipsoid.semiMajorAxis // Equatorial radius, the longer radius of the ellipsoid
val semiMinorAxis = ellipsoid.semiMinorAxis // Polar radius, the shorter radius of the ellipsoid
val eccentricity = ellipsoid.eccentricity // Eccentricity, indicates how close the ellipsoid is to a sphere
val inverseFlattening = ellipsoid.inverseFlattening // Inverse flattening value
val ivfDefinitive = ellipsoid.isIvfDefinitive // Indicates if the inverse flattening is definitive for this ellipsoid

// Orthodromic distance
val orthodromicDistance = ellipsoid.orthodromicDistance(
City.SEOUL.longitude,
City.SEOUL.latitude,
City.BUSAN.longitude,
City.BUSAN.latitude
)

println("isSphere = $isSphere")
println("semiMajorAxis = $semiMajorAxis")
println("semiMinorAxis = $semiMinorAxis")
println("eccentricity = $eccentricity")
println("inverseFlattening = $inverseFlattening")
println("ivfDefinitive = $ivfDefinitive")
println("orthodromicDistance = $orthodromicDistance")
}
}
isSphere = false
semiMajorAxis = 6378137.0
semiMinorAxis = 6356752.314245179
eccentricity = 0.08181919084262128
inverseFlattening = 298.257223563
ivfDefinitive = true
orthodromicDistance = 328199.9794919944

You can create an Earth ellipsoid with DefaultEllipsoid.WGS84. If you use SPHERE instead of WGS84, a sphere with a radius of 6371km will be created.

The distance result is in meters (m), so converting it to kilometers shows approximately 328km. If you search on Google, you may find 325km, so considering that there may be differences between the coordinates I chose and those chosen by Google, this is not a bad figure.

There are many other functions available as well. However, covering them all in this post would be too extensive, so if needed, I will cover them in another post.

info

The margin of error may not be satisfactory depending on business requirements, so before actual implementation, make sure to thoroughly test other methods in geotools.


Footnotes​

  1. An Overview of SRID and Coordinate System ↩

How to Use @JsonNaming

Β· 2 min read
Haril Song
Owner, Software Engineer at 42dot

Sometimes, the JSON naming conventions used in an API may differ from the naming strategy within your application.

{
"Title": "Frozen",
"Year": "2013",
"Type": "movie",
"Poster": "https://m.media-amazon.com/images/M/MV5BMTQ1MjQwMTE5OF5BMl5BanBnXkFtZTgwNjk3MTcyMDE@._V1_SX300.jpg",
"imdbID": "tt2294629"
}
private String title;
private String year;
private String imdbId;
private String type;
private String poster;

If the variable names do not match the JSON keys, the data will not be populated.

In such cases, you can use @JsonProperty(value) to map the data without changing the variable names in the project. However, if there are many fields with different naming strategies, using @JsonProperty(value) on each field can clutter the code with too many annotations.

This is where the @JsonNaming annotation comes in handy, allowing you to change the naming strategy of a class at once.

@JsonNaming​

Before v2.12​

You can elegantly solve this as follows:

@Data
@JsonNaming(value = PropertyNamingStrategy.UpperCamelCaseStrategy.class)
public class Movie {

private String title;
private String year;

@JsonProperty("imdbID") // Only where needed!
private String imdbId;
private String type;
private String poster;

}

image This method is deprecated and marked with a strikethrough.

However, this method has been deprecated since Jackson 2.12, so let's explore the newer approach.

After v2.12​

Starting from version 2.12, you should use PropertyNamingStrategies.

@JsonNaming(value = PropertyNamingStrategies.UpperCamelCaseStrategy.class)

While a detailed explanation of the internal implementation may be too lengthy and off-topic, it is recommended to take a look as it is quite interestingly implemented!

info

In brief, the updated internal implementation involves an abstract class called NamingBase, which inherits the original PropertyNamingStrategy, and then the naming strategy inherits from NamingBase. NamingBase is used as a kind of intermediate implementation class.