diff --git a/spring-boot-modules/spring-boot-graphql/pom.xml b/spring-boot-modules/spring-boot-graphql/pom.xml
index 32399f4123..628babbd3f 100644
--- a/spring-boot-modules/spring-boot-graphql/pom.xml
+++ b/spring-boot-modules/spring-boot-graphql/pom.xml
@@ -13,6 +13,47 @@
1.0.0-SNAPSHOT
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.7.0
+
+
+
+
+ org.xolstice.maven.plugins
+ protobuf-maven-plugin
+ ${protobuf-plugin.version}
+
+ com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
+ grpc-java
+ io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
+
+
+
+
+ compile
+ compile-custom
+
+
+
+
+
+
+
+
+ 3.19.2
+ 0.6.1
+ 1.43.2
+ 2.13.1.RELEASE
+ 1.5.1
+ 1.3.5
+ 1.6.2
+ 3.3.2
+
+
org.springframework.boot
@@ -34,6 +75,27 @@
com.h2database
h2
+
+ io.grpc
+ grpc-stub
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-protobuf
+ ${grpc.version}
+
+
+ jakarta.annotation
+ jakarta.annotation-api
+ ${jakarta.annotation.version}
+ true
+
+
+ net.devh
+ grpc-spring-boot-starter
+ ${grpc.spring.version}
+
org.springframework.boot
spring-boot-starter-test
@@ -49,6 +111,12 @@
spring-graphql-test
test
+
+ org.skyscreamer
+ jsonassert
+ ${jsonassert.version}
+ test
+
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/ChooseApiApp.java b/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/ChooseApiApp.java
new file mode 100644
index 0000000000..ded6a92597
--- /dev/null
+++ b/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/ChooseApiApp.java
@@ -0,0 +1,14 @@
+package com.baeldung.chooseapi;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class ChooseApiApp {
+
+ public static void main(String[] args) {
+ System.setProperty("spring.profiles.default", "chooseapi");
+ SpringApplication.run(ChooseApiApp.class, args);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/controllers/BooksControllerGraphQL.java b/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/controllers/BooksControllerGraphQL.java
new file mode 100644
index 0000000000..1e5f226d45
--- /dev/null
+++ b/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/controllers/BooksControllerGraphQL.java
@@ -0,0 +1,25 @@
+package com.baeldung.chooseapi.controllers;
+
+import com.baeldung.chooseapi.dtos.Book;
+import com.baeldung.chooseapi.services.BooksService;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import org.springframework.graphql.data.method.annotation.QueryMapping;
+
+@RestController
+public class BooksControllerGraphQL {
+
+ private final BooksService booksService;
+
+ public BooksControllerGraphQL(BooksService booksService) {
+ this.booksService = booksService;
+ }
+
+ @QueryMapping
+ public List books() {
+ return booksService.getBooks();
+ }
+
+}
+
diff --git a/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/controllers/BooksControllerRest.java b/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/controllers/BooksControllerRest.java
new file mode 100644
index 0000000000..78cc64b2f4
--- /dev/null
+++ b/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/controllers/BooksControllerRest.java
@@ -0,0 +1,24 @@
+package com.baeldung.chooseapi.controllers;
+
+import com.baeldung.chooseapi.dtos.Book;
+import com.baeldung.chooseapi.services.BooksService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+public class BooksControllerRest {
+
+ private final BooksService booksService;
+
+ public BooksControllerRest(BooksService booksService) {
+ this.booksService = booksService;
+ }
+
+ @GetMapping("/rest/books")
+ public List books() {
+ return booksService.getBooks();
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/dtos/Author.java b/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/dtos/Author.java
new file mode 100644
index 0000000000..498f727ec1
--- /dev/null
+++ b/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/dtos/Author.java
@@ -0,0 +1,40 @@
+package com.baeldung.chooseapi.dtos;
+
+import java.util.Objects;
+
+public class Author {
+
+ private final String firstName;
+ private final String lastName;
+
+ public Author(String firstName, String lastName) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Author author = (Author) o;
+ return firstName.equals(author.firstName) && lastName.equals(author.lastName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(firstName, lastName);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/dtos/Book.java b/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/dtos/Book.java
new file mode 100644
index 0000000000..0fd752c1ba
--- /dev/null
+++ b/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/dtos/Book.java
@@ -0,0 +1,46 @@
+package com.baeldung.chooseapi.dtos;
+
+import java.util.Objects;
+
+public class Book {
+
+ private final String title;
+ private final Author author;
+ private final int year;
+
+ public Book(String title, Author author, int year) {
+ this.title = title;
+ this.author = author;
+ this.year = year;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public Author getAuthor() {
+ return author;
+ }
+
+ public int getYear() {
+ return year;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Book book = (Book) o;
+ return year == book.year && title.equals(book.title) && author.equals(book.author);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(title, author, year);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/grpc/BooksServiceGrpc.java b/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/grpc/BooksServiceGrpc.java
new file mode 100644
index 0000000000..c0b70e2adf
--- /dev/null
+++ b/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/grpc/BooksServiceGrpc.java
@@ -0,0 +1,32 @@
+package com.baeldung.chooseapi.grpc;
+
+import com.baeldung.chooseapi.BooksServiceGrpc.BooksServiceImplBase;
+import com.baeldung.chooseapi.BooksServiceOuterClass.BooksRequest;
+import com.baeldung.chooseapi.BooksServiceOuterClass.BooksResponse;
+
+import com.baeldung.chooseapi.dtos.Book;
+import com.baeldung.chooseapi.services.BooksService;
+import io.grpc.stub.StreamObserver;
+import net.devh.boot.grpc.server.service.GrpcService;
+
+import java.util.List;
+
+@GrpcService
+public class BooksServiceGrpc extends BooksServiceImplBase {
+
+ private final BooksService booksService;
+
+ public BooksServiceGrpc(BooksService booksService) {
+ this.booksService = booksService;
+ }
+
+ @Override
+ public void books(BooksRequest request, StreamObserver responseObserver) {
+ List books = booksService.getBooks();
+ BooksResponse.Builder responseBuilder = BooksResponse.newBuilder();
+ books.forEach(book -> responseBuilder.addBook(GrpcBooksMapper.mapBookToProto(book)));
+ responseObserver.onNext(responseBuilder.build());
+ responseObserver.onCompleted();
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/grpc/GrpcBooksMapper.java b/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/grpc/GrpcBooksMapper.java
new file mode 100644
index 0000000000..f66fb9c16c
--- /dev/null
+++ b/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/grpc/GrpcBooksMapper.java
@@ -0,0 +1,26 @@
+package com.baeldung.chooseapi.grpc;
+
+import com.baeldung.chooseapi.BooksServiceOuterClass;
+import com.baeldung.chooseapi.dtos.Author;
+import com.baeldung.chooseapi.dtos.Book;
+
+public class GrpcBooksMapper {
+
+ public static BooksServiceOuterClass.BookProto mapBookToProto(Book book) {
+ return BooksServiceOuterClass.BookProto.newBuilder()
+ .setTitle(book.getTitle())
+ .setAuthor(BooksServiceOuterClass.AuthorProto.newBuilder()
+ .setFirstName(book.getAuthor().getFirstName())
+ .setLastName(book.getAuthor().getLastName())
+ .build())
+ .setYear(book.getYear())
+ .build();
+ }
+
+ public static Book mapProtoToBook(BooksServiceOuterClass.BookProto bookProto) {
+ return new Book(bookProto.getTitle(),
+ new Author(bookProto.getAuthor().getFirstName(), bookProto.getAuthor().getLastName()),
+ bookProto.getYear());
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/services/BooksService.java b/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/services/BooksService.java
new file mode 100644
index 0000000000..d9094f53fd
--- /dev/null
+++ b/spring-boot-modules/spring-boot-graphql/src/main/java/com/baeldung/chooseapi/services/BooksService.java
@@ -0,0 +1,26 @@
+package com.baeldung.chooseapi.services;
+
+import com.baeldung.chooseapi.dtos.Author;
+import com.baeldung.chooseapi.dtos.Book;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@Service
+public class BooksService {
+
+ private static final Author AUTHOR = new Author("Joanne", "Rowling");
+
+ private static final List BOOKS = new ArrayList<>(Arrays.asList(
+ new Book("Philosopher's Stone", AUTHOR, 1997),
+ new Book("Goblet of Fire", AUTHOR, 2000),
+ new Book("Deathly Hallows", AUTHOR, 2007)
+ ));
+
+ public List getBooks() {
+ return BOOKS;
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-graphql/src/main/proto/BooksService.proto b/spring-boot-modules/spring-boot-graphql/src/main/proto/BooksService.proto
new file mode 100644
index 0000000000..2bd2bd719d
--- /dev/null
+++ b/spring-boot-modules/spring-boot-graphql/src/main/proto/BooksService.proto
@@ -0,0 +1,23 @@
+syntax = "proto3";
+package com.baeldung.chooseapi;
+
+message BooksRequest {}
+
+message AuthorProto {
+ string firstName = 1;
+ string lastName = 2;
+}
+
+message BookProto {
+ string title = 1;
+ AuthorProto author = 2;
+ int32 year = 3;
+}
+
+message BooksResponse {
+ repeated BookProto book = 1;
+}
+
+service BooksService {
+ rpc books(BooksRequest) returns (BooksResponse);
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-graphql/src/main/resources/application-chooseapi.yml b/spring-boot-modules/spring-boot-graphql/src/main/resources/application-chooseapi.yml
new file mode 100644
index 0000000000..889842df9f
--- /dev/null
+++ b/spring-boot-modules/spring-boot-graphql/src/main/resources/application-chooseapi.yml
@@ -0,0 +1,9 @@
+server:
+ port: 8082
+
+spring:
+ graphql:
+ graphiql:
+ enabled: true
+ schema:
+ locations: classpath:chooseapi/
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-graphql/src/main/resources/chooseapi/chooseapi.graphqls b/spring-boot-modules/spring-boot-graphql/src/main/resources/chooseapi/chooseapi.graphqls
new file mode 100644
index 0000000000..5b0e991d48
--- /dev/null
+++ b/spring-boot-modules/spring-boot-graphql/src/main/resources/chooseapi/chooseapi.graphqls
@@ -0,0 +1,14 @@
+type Author {
+ firstName: String!
+ lastName: String!
+}
+
+type Book {
+ title: String!
+ year: Int!
+ author: Author!
+}
+
+type Query {
+ books: [Book]
+}
diff --git a/spring-boot-modules/spring-boot-graphql/src/main/resources/graphql-vs-rest/schema.graphqls b/spring-boot-modules/spring-boot-graphql/src/main/resources/graphql-vs-rest/schema.graphqls
index 520f26648c..e11897fa6c 100644
--- a/spring-boot-modules/spring-boot-graphql/src/main/resources/graphql-vs-rest/schema.graphqls
+++ b/spring-boot-modules/spring-boot-graphql/src/main/resources/graphql-vs-rest/schema.graphqls
@@ -43,7 +43,6 @@ input ProductUpdateModel {
stock: Int
}
-
# The Root Query for the application
type Query {
products(size: Int, page: Int): [Product]!
diff --git a/spring-boot-modules/spring-boot-graphql/src/test/java/com/baeldung/chooseapi/controllers/BooksControllerGraphQLIntegrationTest.java b/spring-boot-modules/spring-boot-graphql/src/test/java/com/baeldung/chooseapi/controllers/BooksControllerGraphQLIntegrationTest.java
new file mode 100644
index 0000000000..c1ce711388
--- /dev/null
+++ b/spring-boot-modules/spring-boot-graphql/src/test/java/com/baeldung/chooseapi/controllers/BooksControllerGraphQLIntegrationTest.java
@@ -0,0 +1,33 @@
+package com.baeldung.chooseapi.controllers;
+
+import com.baeldung.chooseapi.ChooseApiApp;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.graphql.test.tester.HttpGraphQlTester;
+import org.springframework.test.context.ActiveProfiles;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ChooseApiApp.class)
+@ActiveProfiles("chooseapi")
+class BooksControllerGraphQLIntegrationTest {
+
+ @Autowired
+ private HttpGraphQlTester graphQlTester;
+
+ @Test
+ void givenBooksServiceThatReturnThreeBooks_whenCallingGraphQLEndpoint_thenThreeBooksAreReturned() throws Exception {
+ String document = "query { books { title year author { firstName lastName }}}";
+ Path expectedResponse = Paths.get("src/test/resources/graphql-test/books_expected_response.json");
+ String expectedJson = new String(Files.readAllBytes(expectedResponse));
+
+ this.graphQlTester.document(document)
+ .execute()
+ .path("books")
+ .matchesJson(expectedJson);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-graphql/src/test/java/com/baeldung/chooseapi/controllers/BooksControllerRestIntegrationTest.java b/spring-boot-modules/spring-boot-graphql/src/test/java/com/baeldung/chooseapi/controllers/BooksControllerRestIntegrationTest.java
new file mode 100644
index 0000000000..977a132653
--- /dev/null
+++ b/spring-boot-modules/spring-boot-graphql/src/test/java/com/baeldung/chooseapi/controllers/BooksControllerRestIntegrationTest.java
@@ -0,0 +1,36 @@
+package com.baeldung.chooseapi.controllers;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+class BooksControllerRestIntegrationTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ void givenBooksServiceThatReturnThreeBooks_whenCallingRestEndpoint_thenThreeBooksAreReturned() throws Exception {
+ Path expectedResponse = Paths.get("src/test/resources/graphql-test/books_expected_response.json");
+ String expectedJson = new String(Files.readAllBytes(expectedResponse));
+
+ this.mockMvc.perform(get("/rest/books"))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andExpect(content().json(expectedJson));
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-graphql/src/test/java/com/baeldung/chooseapi/grpc/BooksServiceGrpcIntegrationTest.java b/spring-boot-modules/spring-boot-graphql/src/test/java/com/baeldung/chooseapi/grpc/BooksServiceGrpcIntegrationTest.java
new file mode 100644
index 0000000000..e446073cea
--- /dev/null
+++ b/spring-boot-modules/spring-boot-graphql/src/test/java/com/baeldung/chooseapi/grpc/BooksServiceGrpcIntegrationTest.java
@@ -0,0 +1,55 @@
+package com.baeldung.chooseapi.grpc;
+
+import com.baeldung.chooseapi.dtos.Book;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import net.devh.boot.grpc.client.inject.GrpcClient;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import com.baeldung.chooseapi.BooksServiceOuterClass.BooksRequest;
+import com.baeldung.chooseapi.BooksServiceOuterClass.BooksResponse;
+
+import com.baeldung.chooseapi.BooksServiceGrpc.BooksServiceBlockingStub;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@SpringBootTest(properties = {
+ "grpc.server.inProcessName=test", // Enable inProcess server
+ "grpc.server.port=-1", // Disable external server
+ "grpc.client.inProcess.address=in-process:test" // Configure the client to connect to the inProcess server
+})
+@SpringJUnitConfig(GrpcIntegrationTestConfig.class)
+@DirtiesContext // Ensures that the grpc-server is properly shutdown after each test
+class BooksServiceGrpcIntegrationTest {
+
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ @GrpcClient("inProcess")
+ private BooksServiceBlockingStub booksServiceGrpc;
+
+ @Test
+ @DirtiesContext
+ void givenBooksServiceThatReturnThreeBooks_whenCallingGrpcEndpoint_thenThreeBooksAreReturned() throws IOException, JSONException {
+ Path expectedResponse = Paths.get("src/test/resources/graphql-test/books_expected_response.json");
+ String expectedJson = new String(Files.readAllBytes(expectedResponse));
+
+ BooksRequest request = BooksRequest.newBuilder().build();
+ BooksResponse response = booksServiceGrpc.books(request);
+
+ List books = response.getBookList().stream()
+ .map(GrpcBooksMapper::mapProtoToBook)
+ .collect(Collectors.toList());
+
+ JSONAssert.assertEquals(objectMapper.writeValueAsString(books), expectedJson, true);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-graphql/src/test/java/com/baeldung/chooseapi/grpc/GrpcIntegrationTestConfig.java b/spring-boot-modules/spring-boot-graphql/src/test/java/com/baeldung/chooseapi/grpc/GrpcIntegrationTestConfig.java
new file mode 100644
index 0000000000..d70c6e46a5
--- /dev/null
+++ b/spring-boot-modules/spring-boot-graphql/src/test/java/com/baeldung/chooseapi/grpc/GrpcIntegrationTestConfig.java
@@ -0,0 +1,28 @@
+package com.baeldung.chooseapi.grpc;
+
+import com.baeldung.chooseapi.services.BooksService;
+import net.devh.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration;
+import net.devh.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration;
+import net.devh.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration;
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ImportAutoConfiguration({
+ GrpcServerAutoConfiguration.class, // Create required server beans
+ GrpcServerFactoryAutoConfiguration.class, // Select server implementation
+ GrpcClientAutoConfiguration.class}) // Support @GrpcClient annotation
+class GrpcIntegrationTestConfig {
+
+ @Bean
+ BooksService booksService() {
+ return new BooksService();
+ }
+
+ @Bean
+ BooksServiceGrpc booksServiceGrpc() {
+ return new BooksServiceGrpc(booksService());
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-graphql/src/test/resources/graphql-test/books_expected_response.json b/spring-boot-modules/spring-boot-graphql/src/test/resources/graphql-test/books_expected_response.json
new file mode 100644
index 0000000000..066795ee91
--- /dev/null
+++ b/spring-boot-modules/spring-boot-graphql/src/test/resources/graphql-test/books_expected_response.json
@@ -0,0 +1,26 @@
+[
+ {
+ "title": "Philosopher's Stone",
+ "year": 1997,
+ "author": {
+ "firstName": "Joanne",
+ "lastName": "Rowling"
+ }
+ },
+ {
+ "title": "Goblet of Fire",
+ "year": 2000,
+ "author": {
+ "firstName": "Joanne",
+ "lastName": "Rowling"
+ }
+ },
+ {
+ "title": "Deathly Hallows",
+ "year": 2007,
+ "author": {
+ "firstName": "Joanne",
+ "lastName": "Rowling"
+ }
+ }
+]
\ No newline at end of file