diff --git a/aws-lambda/.gitignore b/aws-lambda/.gitignore
new file mode 100644
index 0000000000..9984c2e554
--- /dev/null
+++ b/aws-lambda/.gitignore
@@ -0,0 +1 @@
+.aws-sam/
diff --git a/aws-lambda/pom.xml b/aws-lambda/pom.xml
index 5dc275141d..8014a87126 100644
--- a/aws-lambda/pom.xml
+++ b/aws-lambda/pom.xml
@@ -17,6 +17,7 @@
lambda
shipping-tracker/ShippingFunction
+ todo-reminder/ToDoFunction
diff --git a/aws-lambda/todo-reminder/ToDoFunction/pom.xml b/aws-lambda/todo-reminder/ToDoFunction/pom.xml
new file mode 100644
index 0000000000..f80cbdf22f
--- /dev/null
+++ b/aws-lambda/todo-reminder/ToDoFunction/pom.xml
@@ -0,0 +1,105 @@
+
+ 4.0.0
+ helloworld
+ HelloWorld
+ 1.0
+ jar
+ To Do Application Example.
+
+ 1.8
+ 1.8
+
+
+
+
+ com.amazonaws
+ aws-lambda-java-core
+ 1.2.1
+
+
+ com.amazonaws
+ aws-lambda-java-events
+ 3.6.0
+
+
+ uk.org.webcompere
+ lightweight-config
+ 1.1.0
+
+
+ com.amazonaws
+ aws-lambda-java-log4j2
+ 1.2.0
+
+
+ org.apache.logging.log4j
+ log4j-slf4j-impl
+ 2.13.2
+
+
+ io.github.openfeign
+ feign-core
+ 11.2
+
+
+ io.github.openfeign
+ feign-slf4j
+ 11.2
+
+
+ io.github.openfeign
+ feign-gson
+ 11.2
+
+
+ com.google.inject
+ guice
+ 5.0.1
+
+
+ junit
+ junit
+ 4.13.1
+ test
+
+
+ uk.org.webcompere
+ system-stubs-junit4
+ 1.2.0
+ test
+
+
+ org.mockito
+ mockito-core
+ 3.3.0
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.19.0
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.2.4
+
+
+
+
+ package
+
+ shade
+
+
+
+
+
+
+
diff --git a/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/App.java b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/App.java
new file mode 100644
index 0000000000..b322dabca8
--- /dev/null
+++ b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/App.java
@@ -0,0 +1,43 @@
+package com.baeldung.lambda.todo;
+
+import java.io.*;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import com.baeldung.lambda.todo.config.ExecutionContext;
+import com.baeldung.lambda.todo.service.PostService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler for requests to Lambda function.
+ */
+public class App implements RequestStreamHandler {
+ private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
+
+ private String environmentName = System.getenv("ENV_NAME");
+ private ExecutionContext executionContext = new ExecutionContext();
+
+ @Override
+ public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
+ context.getLogger().log("App starting\n");
+ context.getLogger().log("Environment: "
+ + environmentName + "\n");
+
+ try {
+ PostService postService = executionContext.getPostService();
+ executionContext.getToDoReaderService()
+ .getOldestToDo()
+ .ifPresent(postService::makePost);
+ } catch (Exception e) {
+ LOGGER.error("Failed: {}", e.getMessage(), e);
+ }
+ }
+}
diff --git a/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/api/PostApi.java b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/api/PostApi.java
new file mode 100644
index 0000000000..0521dfe05c
--- /dev/null
+++ b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/api/PostApi.java
@@ -0,0 +1,8 @@
+package com.baeldung.lambda.todo.api;
+
+import feign.RequestLine;
+
+public interface PostApi {
+ @RequestLine("POST /posts")
+ void makePost(PostItem item);
+}
diff --git a/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/api/PostItem.java b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/api/PostItem.java
new file mode 100644
index 0000000000..96049e37d4
--- /dev/null
+++ b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/api/PostItem.java
@@ -0,0 +1,40 @@
+package com.baeldung.lambda.todo.api;
+
+public class PostItem {
+ private String title;
+ private String body;
+ private int userId;
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ }
+
+ public int getUserId() {
+ return userId;
+ }
+
+ public void setUserId(int userId) {
+ this.userId = userId;
+ }
+
+ @Override
+ public String toString() {
+ return "PostItem{"
+ + "title='" + title + '\''
+ + ", body='" + body + '\''
+ + ", userId=" + userId +
+ '}';
+ }
+}
diff --git a/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/api/ToDoApi.java b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/api/ToDoApi.java
new file mode 100644
index 0000000000..bf75cd566d
--- /dev/null
+++ b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/api/ToDoApi.java
@@ -0,0 +1,10 @@
+package com.baeldung.lambda.todo.api;
+
+import feign.RequestLine;
+
+import java.util.List;
+
+public interface ToDoApi {
+ @RequestLine("GET /todos")
+ List getAllTodos();
+}
diff --git a/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/api/ToDoItem.java b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/api/ToDoItem.java
new file mode 100644
index 0000000000..d43b7a947c
--- /dev/null
+++ b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/api/ToDoItem.java
@@ -0,0 +1,50 @@
+package com.baeldung.lambda.todo.api;
+
+public class ToDoItem {
+ private int userId;
+ private int id;
+ private String title;
+ private boolean completed;
+
+ public int getUserId() {
+ return userId;
+ }
+
+ public void setUserId(int userId) {
+ this.userId = userId;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public boolean isCompleted() {
+ return completed;
+ }
+
+ public void setCompleted(boolean completed) {
+ this.completed = completed;
+ }
+
+ @Override
+ public String toString() {
+ return "ToDoItem{"
+ + "userId=" + userId
+ + ", id=" + id
+ + ", title='" + title + '\''
+ + ", completed=" + completed +
+ '}';
+ }
+}
diff --git a/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/config/Config.java b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/config/Config.java
new file mode 100644
index 0000000000..f919ac4006
--- /dev/null
+++ b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/config/Config.java
@@ -0,0 +1,50 @@
+package com.baeldung.lambda.todo.config;
+
+public class Config {
+ private String toDoEndpoint;
+ private String postEndpoint;
+ private String environmentName;
+
+ private Credentials toDoCredentials;
+ private Credentials postCredentials;
+
+ public String getToDoEndpoint() {
+ return toDoEndpoint;
+ }
+
+ public void setToDoEndpoint(String toDoEndpoint) {
+ this.toDoEndpoint = toDoEndpoint;
+ }
+
+ public String getPostEndpoint() {
+ return postEndpoint;
+ }
+
+ public void setPostEndpoint(String postEndpoint) {
+ this.postEndpoint = postEndpoint;
+ }
+
+ public String getEnvironmentName() {
+ return environmentName;
+ }
+
+ public void setEnvironmentName(String environmentName) {
+ this.environmentName = environmentName;
+ }
+
+ public Credentials getToDoCredentials() {
+ return toDoCredentials;
+ }
+
+ public void setToDoCredentials(Credentials toDoCredentials) {
+ this.toDoCredentials = toDoCredentials;
+ }
+
+ public Credentials getPostCredentials() {
+ return postCredentials;
+ }
+
+ public void setPostCredentials(Credentials postCredentials) {
+ this.postCredentials = postCredentials;
+ }
+}
diff --git a/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/config/Credentials.java b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/config/Credentials.java
new file mode 100644
index 0000000000..a11399381c
--- /dev/null
+++ b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/config/Credentials.java
@@ -0,0 +1,22 @@
+package com.baeldung.lambda.todo.config;
+
+public class Credentials {
+ private String username;
+ private String password;
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+}
diff --git a/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/config/ExecutionContext.java b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/config/ExecutionContext.java
new file mode 100644
index 0000000000..b588187abd
--- /dev/null
+++ b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/config/ExecutionContext.java
@@ -0,0 +1,36 @@
+package com.baeldung.lambda.todo.config;
+
+import com.baeldung.lambda.todo.service.PostService;
+import com.baeldung.lambda.todo.service.ToDoReaderService;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ExecutionContext {
+ private static final Logger LOGGER =
+ LoggerFactory.getLogger(ExecutionContext.class);
+
+ private ToDoReaderService toDoReaderService;
+ private PostService postService;
+
+ public ExecutionContext() {
+ LOGGER.info("Loading configuration");
+
+ try {
+ Injector injector = Guice.createInjector(new Services());
+ this.toDoReaderService = injector.getInstance(ToDoReaderService.class);
+ this.postService = injector.getInstance(PostService.class);
+ } catch (Exception e) {
+ LOGGER.error("Could not start", e);
+ }
+ }
+
+ public ToDoReaderService getToDoReaderService() {
+ return toDoReaderService;
+ }
+
+ public PostService getPostService() {
+ return postService;
+ }
+}
diff --git a/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/config/Services.java b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/config/Services.java
new file mode 100644
index 0000000000..c1d85012eb
--- /dev/null
+++ b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/config/Services.java
@@ -0,0 +1,38 @@
+package com.baeldung.lambda.todo.config;
+
+import com.baeldung.lambda.todo.api.PostApi;
+import com.baeldung.lambda.todo.api.ToDoApi;
+import com.google.inject.AbstractModule;
+import feign.Feign;
+import feign.auth.BasicAuthRequestInterceptor;
+import feign.gson.GsonDecoder;
+import feign.gson.GsonEncoder;
+import feign.slf4j.Slf4jLogger;
+import uk.org.webcompere.lightweightconfig.ConfigLoader;
+
+import static feign.Logger.Level.FULL;
+
+public class Services extends AbstractModule {
+ @Override
+ protected void configure() {
+ Config config = ConfigLoader.loadYmlConfigFromResource("configuration.yml", Config.class);
+
+ ToDoApi toDoApi = Feign.builder()
+ .decoder(new GsonDecoder())
+ .logger(new Slf4jLogger())
+ .logLevel(FULL)
+ .requestInterceptor(new BasicAuthRequestInterceptor(config.getToDoCredentials().getUsername(), config.getToDoCredentials().getPassword()))
+ .target(ToDoApi.class, config.getToDoEndpoint());
+
+ PostApi postApi = Feign.builder()
+ .encoder(new GsonEncoder())
+ .logger(new Slf4jLogger())
+ .logLevel(FULL)
+ .requestInterceptor(new BasicAuthRequestInterceptor(config.getPostCredentials().getUsername(), config.getPostCredentials().getPassword()))
+ .target(PostApi.class, config.getPostEndpoint());
+
+ bind(Config.class).toInstance(config);
+ bind(ToDoApi.class).toInstance(toDoApi);
+ bind(PostApi.class).toInstance(postApi);
+ }
+}
diff --git a/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/service/PostService.java b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/service/PostService.java
new file mode 100644
index 0000000000..bb0a67dbd1
--- /dev/null
+++ b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/service/PostService.java
@@ -0,0 +1,30 @@
+package com.baeldung.lambda.todo.service;
+
+import com.baeldung.lambda.todo.api.PostApi;
+import com.baeldung.lambda.todo.api.PostItem;
+import com.baeldung.lambda.todo.api.ToDoItem;
+import com.google.inject.Inject;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+
+public class PostService {
+ private static final Logger LOGGER = LogManager.getLogger(PostService.class);
+ private PostApi postApi;
+
+ @Inject
+ public PostService(PostApi postApi) {
+ this.postApi = postApi;
+ }
+
+ public void makePost(ToDoItem toDoItem) {
+ LOGGER.info("Posting about: {}", toDoItem);
+ PostItem item = new PostItem();
+ item.setTitle("To Do is Out Of Date: " + toDoItem.getId());
+ item.setUserId(toDoItem.getUserId());
+ item.setBody("Not done: " + toDoItem.getTitle());
+
+ LOGGER.info("Post: {}", item);
+ postApi.makePost(item);
+ }
+}
diff --git a/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/service/ToDoReaderService.java b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/service/ToDoReaderService.java
new file mode 100644
index 0000000000..5cd18ff46a
--- /dev/null
+++ b/aws-lambda/todo-reminder/ToDoFunction/src/main/java/com/baeldung/lambda/todo/service/ToDoReaderService.java
@@ -0,0 +1,30 @@
+package com.baeldung.lambda.todo.service;
+
+import com.baeldung.lambda.todo.api.ToDoApi;
+import com.baeldung.lambda.todo.api.ToDoItem;
+import com.baeldung.lambda.todo.config.Config;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.inject.Inject;
+import java.util.Optional;
+
+public class ToDoReaderService {
+ // Log4j logger
+ private static final Logger LOGGER = LogManager.getLogger(ToDoReaderService.class);
+
+ private ToDoApi toDoApi;
+
+ @Inject
+ public ToDoReaderService(Config configuration, ToDoApi toDoApi) {
+ LOGGER.info("ToDo Endpoint on: {}", configuration.getToDoEndpoint());
+
+ this.toDoApi = toDoApi;
+ }
+
+ public Optional getOldestToDo() {
+ return toDoApi.getAllTodos().stream()
+ .filter(item -> !item.isCompleted())
+ .findFirst();
+ }
+}
diff --git a/aws-lambda/todo-reminder/ToDoFunction/src/main/resources/configuration.yml b/aws-lambda/todo-reminder/ToDoFunction/src/main/resources/configuration.yml
new file mode 100644
index 0000000000..f67239bc23
--- /dev/null
+++ b/aws-lambda/todo-reminder/ToDoFunction/src/main/resources/configuration.yml
@@ -0,0 +1,9 @@
+toDoEndpoint: https://jsonplaceholder.typicode.com
+postEndpoint: https://jsonplaceholder.typicode.com
+environmentName: ${ENV_NAME}
+toDoCredentials:
+ username: baeldung
+ password: ${TODO_PASSWORD:-password}
+postCredentials:
+ username: baeldung
+ password: ${POST_PASSWORD:-password}
diff --git a/aws-lambda/todo-reminder/ToDoFunction/src/main/resources/log4j2.xml b/aws-lambda/todo-reminder/ToDoFunction/src/main/resources/log4j2.xml
new file mode 100644
index 0000000000..b241e5d167
--- /dev/null
+++ b/aws-lambda/todo-reminder/ToDoFunction/src/main/resources/log4j2.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1} - %m%n
+
+
+
+
+
+
+
+
+
diff --git a/aws-lambda/todo-reminder/ToDoFunction/src/test/java/com/baeldung/lambda/todo/AppTest.java b/aws-lambda/todo-reminder/ToDoFunction/src/test/java/com/baeldung/lambda/todo/AppTest.java
new file mode 100644
index 0000000000..cbdc8c22cb
--- /dev/null
+++ b/aws-lambda/todo-reminder/ToDoFunction/src/test/java/com/baeldung/lambda/todo/AppTest.java
@@ -0,0 +1,58 @@
+package com.baeldung.lambda.todo;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.baeldung.lambda.todo.config.Config;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import uk.org.webcompere.lightweightconfig.ConfigLoader;
+import uk.org.webcompere.systemstubs.rules.EnvironmentVariablesRule;
+import uk.org.webcompere.systemstubs.stream.input.LinesAltStream;
+import uk.org.webcompere.systemstubs.stream.output.NoopStream;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.verify;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AppTest {
+
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private Context mockContext;
+
+ @Rule
+ public EnvironmentVariablesRule environmentVariablesRule = new EnvironmentVariablesRule();
+
+ private InputStream fakeInputStream = new LinesAltStream();
+ private OutputStream fakeOutputStream = new NoopStream();
+
+ @Test
+ public void whenTheEnvironmentVariableIsSet_thenItIsLogged() throws Exception {
+ environmentVariablesRule.set("ENV_NAME", "unitTest");
+ new App().handleRequest(fakeInputStream, fakeOutputStream, mockContext);
+
+ verify(mockContext.getLogger())
+ .log("Environment: unitTest\n");
+ }
+
+ @Test
+ public void givenEnvironmentVariableIsNotSet_thenUseDefault() {
+ String setting = Optional.ofNullable(System.getenv("SETTING"))
+ .orElse("default");
+
+ assertThat(setting).isEqualTo("default");
+ }
+
+ @Test
+ public void givenConfiguration_canLoadIntoPojo() {
+ environmentVariablesRule.set("ENV_NAME", "unitTest");
+ Config config = ConfigLoader.loadYmlConfigFromResource("configuration.yml", Config.class);
+ assertThat(config.getEnvironmentName()).isEqualTo("unitTest");
+ }
+}
\ No newline at end of file
diff --git a/aws-lambda/todo-reminder/ToDoFunction/src/test/java/com/baeldung/lambda/todo/service/ToDoReaderServiceTest.java b/aws-lambda/todo-reminder/ToDoFunction/src/test/java/com/baeldung/lambda/todo/service/ToDoReaderServiceTest.java
new file mode 100644
index 0000000000..634c5257ff
--- /dev/null
+++ b/aws-lambda/todo-reminder/ToDoFunction/src/test/java/com/baeldung/lambda/todo/service/ToDoReaderServiceTest.java
@@ -0,0 +1,24 @@
+package com.baeldung.lambda.todo.service;
+
+import com.baeldung.lambda.todo.config.Config;
+import org.junit.Rule;
+import org.junit.Test;
+import uk.org.webcompere.systemstubs.rules.SystemOutRule;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+
+public class ToDoReaderServiceTest {
+
+ @Rule
+ public SystemOutRule systemOutRule = new SystemOutRule();
+
+ @Test
+ public void whenTheServiceStarts_thenItOutputsEndpoint() {
+ Config config = new Config();
+ config.setToDoEndpoint("https://todo-endpoint.com");
+ ToDoReaderService service = new ToDoReaderService(config, null);
+
+ assertThat(systemOutRule.getLinesNormalized())
+ .contains("ToDo Endpoint on: https://todo-endpoint.com");
+ }
+}
\ No newline at end of file
diff --git a/aws-lambda/todo-reminder/template.yaml b/aws-lambda/todo-reminder/template.yaml
new file mode 100644
index 0000000000..f32ee392c5
--- /dev/null
+++ b/aws-lambda/todo-reminder/template.yaml
@@ -0,0 +1,22 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Transform: AWS::Serverless-2016-10-31
+Description: todo-reminder application
+
+Parameters:
+ EnvironmentName:
+ Type: String
+ Default: dev
+
+Resources:
+ ToDoFunction:
+ Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
+ Properties:
+ Timeout: 20
+ CodeUri: ToDoFunction
+ Handler: com.baeldung.lambda.todo.App::handleRequest
+ Runtime: java8
+ MemorySize: 512
+ Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object
+ Variables:
+ PARAM1: VALUE
+ ENV_NAME: !Ref EnvironmentName
\ No newline at end of file