diff --git a/pom.xml b/pom.xml index ef3a39265d53..9fde5a87122b 100644 --- a/pom.xml +++ b/pom.xml @@ -201,6 +201,7 @@ currying serialized-entity identity-map + row-data-gateway component context-object optimistic-offline-lock @@ -218,6 +219,7 @@ function-composition microservices-distributed-tracing microservices-idempotent-consumer + row-data-gateway diff --git a/row-data-gateway/README.md b/row-data-gateway/README.md new file mode 100644 index 000000000000..39b0430ce8c6 --- /dev/null +++ b/row-data-gateway/README.md @@ -0,0 +1,99 @@ + +--- +title: "Row Data Pattern: Ensuring Efficient Data Handling in Java" +shortTitle: Row Data +description: "Explore the Row Data Pattern in Java for handling large sets of data efficiently and ensuring proper management of row-level operations in distributed systems." +category: Data Management +language: en +tag: +- Performance +- Data Processing +- Scalability +- Microservices +- Data Integrity +--- + +## Intent of Row Data Pattern + +To manage large datasets efficiently and ensure row-level operations are optimized, reducing memory overhead in distributed systems. + +## Detailed Explanation of Row Data Pattern + +### Real-world example + +> Imagine a system that processes large batches of customer data. Each customer record is handled as a row in the database. With the Row Data Pattern, operations such as data validation, updates, or deletions are performed at the row level, ensuring minimal resource consumption and better scalability. + +### In plain words + +> The Row Data Pattern focuses on processing and managing individual rows of data efficiently, ensuring that large datasets are handled without compromising system performance. + +## Programmatic Example of Row Data Pattern in Java + +The Row Data Pattern can be implemented by iterating over each row in a dataset, performing necessary operations, and ensuring that each row is processed independently, allowing for better scalability. + +**Snippet 1: Process Data Rows** + +```java +public void processRows(List rows) { + for (RowData row : rows) { + processRow(row); + } +} + +private void processRow(RowData row) { + // Perform row-level operations such as validation or transformation +} +``` + +**Snippet 2: Handling Large Data Sets** + +```java +public void handleLargeDataSet(List largeDataSet) { + for (int i = 0; i < largeDataSet.size(); i++) { + processRow(largeDataSet.get(i)); + } +} +``` + +### Key Components of Row Data Pattern + +1. **Row**: Represents a single unit of data in a dataset. Operations are applied to individual rows, making it more memory efficient. +2. **Row Processor**: Handles the logic for processing or transforming the data in each row. +3. **Dataset**: A collection of rows that need to be processed or updated. + +## When to Use the Row Data Pattern in Java + +* When dealing with large datasets that need to be processed row by row. +* When memory optimization is a priority in handling large amounts of data. +* In systems that require efficient batch processing without overwhelming system resources. + +## Real-World Applications of Row Data Pattern + +* Financial systems processing individual transactions. +* Data analytics platforms handling large-scale data processing. +* Microservices managing row-level database operations for customer records. + +## Benefits and Trade-offs of Row Data Pattern + +Benefits: + +* Memory-efficient when handling large datasets. +* Scalable for batch processing in distributed systems. + +Trade-offs: + +* Might introduce performance overhead for complex row operations. +* Requires careful management of row-level operations to ensure efficiency. + +## Related Java Design Patterns + +* [Iterator Pattern](https://java-design-patterns.com/patterns/iterator/): Used to iterate over collections, which complements the Row Data Pattern in handling individual data elements. +* [Batch Processing](https://java-design-patterns.com/patterns/batch-processing/): A pattern for managing large batches of data, working well with Row Data for scalable data operations. + +## References and Credits + +* [Java Design Patterns](https://java-design-patterns.com/) +* [Design Patterns: Elements of Reusable Object-Oriented Software](https://amzn.to/3y6yv1z) +``` + +This README provides a high-level overview of the Row Data Pattern, illustrating how it optimizes large dataset handling, with sample Java code. For additional details, refer to [Java Design Patterns](https://java-design-patterns.com/). \ No newline at end of file diff --git a/row-data-gateway/etc/row data.puml b/row-data-gateway/etc/row data.puml new file mode 100644 index 000000000000..4ef60882a8c1 --- /dev/null +++ b/row-data-gateway/etc/row data.puml @@ -0,0 +1,33 @@ +@startuml +package com.iluwatar.rowdata { + class UserGateway { + - rowdata : RowData + - connection : Connection + - logger : Logger + + UserGateway(RowData rowData, Connection connection) + + RowData read() + + void insert() + + void update() + + void delete() + } + + class RowData { + - int id + - String name + - int value + + int getId() + + String getName() + + int getValue() + } + + UserGateway --> RowData : "uses" + + + class App { + + main(args : String[]) : void + } + App --> RowData : "uses" + App --> UserGateway : "uses" + } +} +@enduml \ No newline at end of file diff --git a/row-data-gateway/pom.xml b/row-data-gateway/pom.xml new file mode 100644 index 000000000000..b3649843bd2f --- /dev/null +++ b/row-data-gateway/pom.xml @@ -0,0 +1,108 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + row-data-gateway + + + + org.slf4j + slf4j-api + 1.7.36 + + + ch.qos.logback + logback-classic + 1.4.12 + + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + junit + junit + test + + + org.xerial + sqlite-jdbc + 3.36.0.3 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.1 + + 17 + 17 + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + make-assembly + package + + single + + + + + com.iluwatar.rowdata.App + + + + jar-with-dependencies + + + + + + + + \ No newline at end of file diff --git a/row-data-gateway/src/main/java/com/iluwatar/rowdata/App.java b/row-data-gateway/src/main/java/com/iluwatar/rowdata/App.java new file mode 100644 index 000000000000..8e151e28a971 --- /dev/null +++ b/row-data-gateway/src/main/java/com/iluwatar/rowdata/App.java @@ -0,0 +1,142 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.rowdata; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +/** + * Main application to interact with the database. + */ +public class App { + private static final Logger logger = LoggerFactory.getLogger(App.class); + private static Connection connection; + + /** + * Initializes the database connection. + */ + public static void initialize() { + try { + connection = DriverManager.getConnection("jdbc:sqlite:/path/to/sample.db"); + } catch (SQLException e) { + throw new DatabaseOperationException("Failed to initialize the database connection", e); + } + } + + /** + * Returns the active database connection. + * + * @return the active connection + */ + public static Connection getConnection() { + if (connection == null) { + throw new IllegalStateException("Connection is not initialized. Call initialize() first."); + } + return connection; + } + + /** + * Closes the database connection. + */ + public static void closeConnection() { + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + throw new DatabaseOperationException("Failed to close the database connection", e); + } + } + } + + /** + * Creates the rowDataTable if it doesn't exist. + */ + public static void createTable() { + String sql = "CREATE TABLE IF NOT EXISTS rowDataTable " + + "(ID INTEGER PRIMARY KEY AUTOINCREMENT, " + + "NAME TEXT NOT NULL, " + + "VALUE INTEGER NOT NULL)"; + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate(sql); + logger.info("Table created or already exists."); + } catch (SQLException e) { + logger.error("Error creating table: {}", e.getMessage()); + } + } + + /** + * Displays the content of the rowDataTable. + */ + public static void display() { + String sql = "SELECT ID, NAME FROM rowDataTable"; + try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { + while (rs.next()) { + int id = rs.getInt("ID"); + String name = rs.getString("NAME"); + int value = rs.getInt("VALUE"); + if (logger.isInfoEnabled()) { + logger.info("ID = {}, NAME = {}, VALUE = {}", id, name, value); + } + } + } catch (SQLException e) { + logger.error("Error displaying table: {}", e.getMessage()); + } + } + /** + * Starting point for the program. + * @param args command line args + * @throws SQLException if any error occur, since SQL code is necessary in {@link UserGateway} + */ + public static void main(String[] args) { + initialize(); + createTable(); + + RowData row1 = new RowData(1, "John", 20); + RowData row2 = new RowData(2, "Mary", 30); + RowData row3 = new RowData(3, "Doe", 40); + + UserGateway rowGateway1 = new UserGateway(row1, connection); + UserGateway rowGateway2 = new UserGateway(row2, connection); + UserGateway rowGateway3 = new UserGateway(row3, connection); + + rowGateway1.insert(); + rowGateway2.insert(); + rowGateway3.insert(); + display(); + + row3.setName("Dorothy"); + rowGateway3.setRowData(row3); + rowGateway3.update(); + display(); + + rowGateway2.delete(); + display(); + + closeConnection(); + } +} diff --git a/row-data-gateway/src/main/java/com/iluwatar/rowdata/DatabaseOperationException.java b/row-data-gateway/src/main/java/com/iluwatar/rowdata/DatabaseOperationException.java new file mode 100644 index 000000000000..4d050ea319ea --- /dev/null +++ b/row-data-gateway/src/main/java/com/iluwatar/rowdata/DatabaseOperationException.java @@ -0,0 +1,15 @@ +package com.iluwatar.rowdata; +/** + * Custom exception for database operation errors. + */ +public class DatabaseOperationException extends RuntimeException { + /** + * Constructs a new DatabaseOperationException with the specified detail message and cause. + * + * @param message the detail message + * @param cause the cause + */ + public DatabaseOperationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/row-data-gateway/src/main/java/com/iluwatar/rowdata/RowData.java b/row-data-gateway/src/main/java/com/iluwatar/rowdata/RowData.java new file mode 100644 index 000000000000..925781375836 --- /dev/null +++ b/row-data-gateway/src/main/java/com/iluwatar/rowdata/RowData.java @@ -0,0 +1,140 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.rowdata; + +import lombok.Getter; +import lombok.Setter; + +/** + * The RowData class represents a single row of data in the database. + * It contains properties like ID, Name, and Value, along with getters and setters for each property. + */ +@Getter +@Setter +public class RowData { + + private int id; + private String name; + private int value; + + /** + * Constructor to initialize RowData object with the specified values. + * + * @param id the ID of the row + * @param name the name of the row + * @param value the value of the row + */ + public RowData(int id, String name, int value) { + this.id = id; + this.name = name; + this.value = value; + } + + /** + * Gets the current name of the RowData. + * + * @return the name of the row + */ + public String getName() { + return name; + } + + /** + * Sets the name of the RowData. + * + * @param name the new name of the row + */ + public void setName(String name) { + this.name = name; + } + + /** + * Gets the current value of the RowData. + * + * @return the value of the row + */ + public int getValue() { + return value; + } + + /** + * Sets the value of the RowData. + * + * @param value the new value of the row + */ + public void setValue(int value) { + this.value = value; + } + + /** + * Gets the current ID of the RowData. + * + * @return the ID of the row + */ + public int getId() { + return id; + } + + /** + * Sets the ID of the RowData. + * + * @param id the new ID of the row + */ + public void setId(int id) { + this.id = id; + } + + /** + * Compares two RowData objects for equality. + * Rows are considered equal if their ID, name, and value are identical. + * + * @param obj the object to compare this RowData to + * @return true if the RowData objects are equal, false otherwise + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + RowData rowData = (RowData) obj; + return id == rowData.id && value == rowData.value && name.equals(rowData.name); + } + + /** + * Returns a hash code for the RowData object, based on its ID, name, and value. + * + * @return the hash code for the RowData + */ + @Override + public int hashCode() { + int result = Integer.hashCode(id); + result = 31 * result + name.hashCode(); + result = 31 * result + Integer.hashCode(value); + return result; + } +} diff --git a/row-data-gateway/src/main/java/com/iluwatar/rowdata/UserGateway.java b/row-data-gateway/src/main/java/com/iluwatar/rowdata/UserGateway.java new file mode 100644 index 000000000000..2103d0eb9312 --- /dev/null +++ b/row-data-gateway/src/main/java/com/iluwatar/rowdata/UserGateway.java @@ -0,0 +1,137 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.rowdata; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; +import lombok.Getter; +import lombok.Setter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +/** + * This class is responsible for performing CRUD operations on RowData. + * It provides methods to insert, update, and delete rows in the rowDataTable. + */ +@Getter +@Setter +public class UserGateway { + + private static final Logger logger = LoggerFactory.getLogger(UserGateway.class); + + @Getter + @Setter + private RowData rowData; + + private Connection connection; + + /** + * Constructs a UserGateway with a RowData object and a database connection. + * + * @param rowData the RowData object to be operated on. + * @param connection the database connection to interact with. + */ + public UserGateway(RowData rowData, Connection connection) { + this.rowData = rowData; + this.connection = connection; + } + + /** + * Reads a row from the database by ID. + * + * @return the retrieved RowData object, or null if no matching row is found. + */ + public Optional read() { + String sql = "SELECT ID, NAME, VALUE FROM rowDataTable WHERE ID = ?"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setInt(1, rowData.getId()); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + int id = rs.getInt("ID"); + String name = rs.getString("NAME"); + int value = rs.getInt("VALUE"); + logger.info("Row retrieved: ID={}, NAME={}, VALUE={}", id, name, value); + return Optional.of(new RowData(id, name, value)); + } + } + } catch (SQLException e) { + logger.error("Error reading row: {}", e.getMessage()); + } + return Optional.empty(); + } + + + /** + * Inserts the current RowData object into the rowDataTable. + * Logs the SQL query upon successful insertion. + */ + public void insert() { + String sql = "INSERT INTO rowDataTable (NAME, VALUE) VALUES (?, ?)"; + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + pstmt.setString(1, rowData.getName()); + pstmt.setInt(2, rowData.getValue()); + pstmt.executeUpdate(); + logger.info("Row inserted with name: {} and value: {}", rowData.getName(), + rowData.getValue()); + } catch (SQLException e) { + logger.error("Error inserting row: {}", e.getMessage()); + } + } + + /** + * Updates the row in rowDataTable corresponding to the current RowData object. + * The row is updated based on the ID of the RowData object. + * Logs the SQL query upon successful update. + */ + public void update() { + String sql = "UPDATE rowDataTable SET NAME = ?, VALUE = ? WHERE ID = ?"; + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + pstmt.setString(1, rowData.getName()); + pstmt.setInt(2, rowData.getValue()); + pstmt.setInt(3, rowData.getId()); + int affectedRows = pstmt.executeUpdate(); + logger.info("Row updated, affected rows: {}", affectedRows); + } catch (SQLException e) { + logger.error("Error updating row: {}", e.getMessage()); + } + } + + /** + * Deletes the row in rowDataTable corresponding to the current RowData object. + * The row is deleted based on the ID of the RowData object. + * Logs the SQL query upon successful deletion. + */ + public void delete() { + String sql = "DELETE FROM rowDataTable WHERE ID = ?"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setInt(1, rowData.getId()); + int rowsAffected = stmt.executeUpdate(); + logger.info("Row deleted. Rows affected: {}", rowsAffected); + } catch (SQLException e) { + logger.error("Error deleting row: {}", e.getMessage()); + } + } +} diff --git a/row-data-gateway/src/test/java/com/iluwatar/rowdata/UserGatewayTest.java b/row-data-gateway/src/test/java/com/iluwatar/rowdata/UserGatewayTest.java new file mode 100644 index 000000000000..e042de0fcb8b --- /dev/null +++ b/row-data-gateway/src/test/java/com/iluwatar/rowdata/UserGatewayTest.java @@ -0,0 +1,84 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.rowdata; +import org.junit.jupiter.api.Test; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import static org.junit.jupiter.api.Assertions.*; + +class UserGatewayTest { + + @Test + void insertTest() { + var r1 = new RowData(1, "John", 25); + try (Connection conn = DriverManager.getConnection("jdbc:sqlite:test.db")) { + UserGateway userGateway = new UserGateway(r1, conn); + assertDoesNotThrow(userGateway::insert); + } catch (SQLException e) { + fail("Database connection failed"); + } + } + + @Test + void updateTest() { + var r1 = new RowData(1, "John", 25); + var r2 = new RowData(1, "Johnny", 30); + try (Connection conn = DriverManager.getConnection("jdbc:sqlite:test.db")) { + UserGateway userGateway = new UserGateway(r1, conn); + userGateway.insert(); + userGateway.setRowData(r2); + assertDoesNotThrow(userGateway::update); + } catch (SQLException e) { + fail("Database connection failed"); + } + } + + @Test + void deleteTest() { + var r1 = new RowData(1, "John", 25); + try (Connection conn = DriverManager.getConnection("jdbc:sqlite:test.db")) { + UserGateway userGateway = new UserGateway(r1, conn); + userGateway.insert(); + assertDoesNotThrow(userGateway::delete); + } catch (SQLException e) { + fail("Database connection failed"); + } + } + + + @Test + void readTest() { + var r1 = new RowData(1, "John", 25); + var r2 = new RowData(1, "Johnny", 30); + try (Connection conn = DriverManager.getConnection("jdbc:sqlite:test.db")) { + UserGateway userGateway = new UserGateway(r1, conn); + userGateway.setRowData(r2); + assertEquals(r2, userGateway.getRowData()); + } catch (SQLException e) { + fail("Database connection failed"); + } + } +} diff --git a/update-header.sh b/update-header.sh index 48da4dcd6125..568d00d52a03 100755 --- a/update-header.sh +++ b/update-header.sh @@ -1,4 +1,29 @@ #!/bin/bash +# +# This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). +# +# The MIT License +# Copyright © 2014-2022 Ilkka Seppälä +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + # Find all README.md files in subdirectories one level deep # and replace "### " with "## " at the beginning of lines