Instant ebooks textbook Introducing Play Framework Java Web Application Development Second Edition Prem Kumar Karunakaran download all chapters
Instant ebooks textbook Introducing Play Framework Java Web Application Development Second Edition Prem Kumar Karunakaran download all chapters
com
https://textbookfull.com/product/introducing-play-framework-
java-web-application-development-second-edition-prem-kumar-
karunakaran/
OR CLICK BUTTON
DOWNLOAD NOW
https://textbookfull.com/product/learn-java-for-web-development-
modern-java-web-development-layka-vishal/
textboxfull.com
https://textbookfull.com/product/methodologies-and-application-issues-
of-contemporary-computing-framework-jyotsna-kumar-mandal/
textboxfull.com
https://textbookfull.com/product/building-web-apps-with-wordpress-
wordpress-as-an-application-framework-brian-messenlehner/
textboxfull.com
https://textbookfull.com/product/pyside-gui-application-development-
second-edition-gopinath-jaganmohan/
textboxfull.com
Introducing Maven A Build Tool for Today s Java Developers
Second Edition Balaji Varanasi
https://textbookfull.com/product/introducing-maven-a-build-tool-for-
today-s-java-developers-second-edition-balaji-varanasi/
textboxfull.com
https://textbookfull.com/product/java-ee-development-with-eclipse-
second-edition-ram-kulkarni/
textboxfull.com
https://textbookfull.com/product/introducing-bootstrap-4-create-
powerful-web-applications-using-bootstrap-4-5-second-edition-joerg-
krause/
textboxfull.com
https://textbookfull.com/product/pro-typescript-application-scale-
javascript-development-second-edition-fenton/
textboxfull.com
While the advice and information in this book are believed to be true
and accurate at the date of publication, neither the authors nor the
editors nor the publisher can accept any legal responsibility for any
errors or omissions that may be made. The publisher makes no
warranty, express or implied, with respect to the material contained
herein.
Further References
Play Framework official docs: www.playframework.com/
Google groups for Play:
https://groups.google.com/forum/#!forum/play-
framework
Questions related to Play:
http://stackoverflow.com/tags/playframework
Ebeans: www.avaje.org/
Twirl: https://github.com/playframework/twirl
Table of Contents
Chapter 1:Getting Started with Play 2
Getting Ready
Installation
Prerequisites
Installing sbt
Installing conscript
Installing Giter8
Setting Up Play
Using Play Example Projects
Using sbt
Creating Your First Project
app
conf
build.sbt
project
public
lib
test
Configuring Play to Work with Your Preferred IDE
Setting Up in Eclipse
Setting Up in IntelliJ
Hello World Application
Configuration
Controller and View
Testing Play Applications
Testing Views
Testing Controllers
Chapter 2:Build System
Scala Build Tool/Simple Build Tool
Core Principles
Benefits of sbt
Project Structure
Using sbt
Setting Definition
Resolvers
Complete build.sbt
Complete plugins.sbt
Quick Recap of SBT Commands
Chapter 3:Play Controllers and HTTP Routing
MVC Programming Model
Model
View
Controller
HTTP Routing
Static Definition
Dynamic Parts in a URL
Passing Fixed Values
Optional Parameters
Application Configuration Using application.conf
Controllers
Finishing the Bookshop Controller
saveComment Method
Testing the saveComment Action
Models
Scoped Objects
Session Scope
Flash Scope
Chapter 4:Play Views and Templating with Scala
Composite Views
Designing a General Template
Code Snippets Templating Basics
Comments
Template Parameters
Import Statement
Iterating a List
Iterating a Map
If Blocks
Escaping Dynamic Contents
Chapter 5:Concurrency and Asynchronous Programming
What Is Concurrency?
Executor
Example 1:Using Runnable
Example 2:Using Callable
Asynchronous Programming with Play
Writing an Asynchronous App
Configuring Asynchronous Scheduled Jobs
Akka Basics
Chapter 6:Web Services, JSON, and XML
Consuming Web Services
Processing Large Responses
Handling JSON
Consuming JSON Request
Producing a JSON Response
Handling XML
Example 1:Simple XML Parsing
Example 2:XML Parsing Using JAXB
Chapter 7:Accessing Databases
Configuring Database Support
Working with an ORM
ORM Concepts
Key Terms
Relationship Direction
Configuring JPA
Using Ebean in Play
Ebean Query
Common Select Query Constructs in Ebean
Using RawSql
Relationships in Ebean
Chapter 8:Complete Example
Chapter 9:Using Play Modules
Creating a Module
Third-Party Modules
Chapter 10:Application Settings and Error Handling
Filters
Action Composition
Error Handlers
Client Errors
Server Errors
How Global Settings Were Done Before Play 2.6.x
Chapter 11:Working with Cache
Configuring Caffeine
Adding Caffeine to a Project
Configuring EhCache
Using the Cache API
Chapter 12:Production Deployment
Configuring Apache httpd for Play
Load Balancing Using mod_proxy_balancer
Configuring Play with Nginx
Index
About the Author
Prem Kumar Karunakaran
is an enterprise architect with about 20 years of industry experience.
He holds a M.Tech in Software Systems from BITS Pilani and a
bachelor’s degree in electronics engineering from Cochin University of
Science and Technology. He is also an Oracle Certified Java Enterprise
Edition Master. He was involved in the architecture and design of many
cutting-edge products used by clients around the globe. He has worked
with organizations such as Infosys and IBS as an architect and has
worked in many projects spanning airlines, logistics, travel, and retail
domain. He is passionate about Java, Machine Learning, BigData
processing and Cloud and loves to learn new technologies; he
contributes his time to open source initiatives as well.
About the Technical Reviewer
Satheesh Madhavan
has nearly 20 years of experience in the software industry and
currently serves as a digital consultant in one of India’s largest IT firms.
He has experience working in Java and JEE technologies in providing
enterprise solutions under various capacities. His qualifications
include an M.S in software systems from BITS Pilani and a B.Tech in
chemical engineering from the University of Kerala. His hobbies
include reading fiction and non-fiction, Philately, technical blogs and
podcasts, and astrophysics and pure sciences.
© Prem Kumar Karunakaran 2020
P. K. Karunakaran, Introducing Play Framework
https://doi.org/10.1007/978-1-4842-5645-9_1
After reading the introductory section on Play 2, you should now have an idea of the capabilities of Play
Framework and how it can accelerate Java web development. This chapter is about
Installing Play Framework
Setting up Play Framework on your machine
Configuring Play Framework
Creating the first sample project
Setting up the IDE
Getting Ready
All you need is a browser and Internet connectivity. You can install Play on a wide variety of operating
systems, including Microsoft Windows, Linux, and Mac. The operating system should have Java installed.
Installation
Play just needs the Play jars available at runtime to work, hence you can include the Play jars in any
application using Maven or any such build tool.
But the recommended way to use Play is to install it using either sbt or Gradle because Play provides
a better development experience when using sbt or Gradle.
Prerequisites
Play 2.x requires JDK 1.8 or later to be installed on the machine. Please note that JRE is not enough; you
need JDK Version 8 or higher. You should ensure that the PATH variable points to the JDK bin directory
and that javac and Java are accessible from everywhere.
You can check this by typing javac -version in the command prompt or shell and verify that the
version is 1.8 or above.
You can get Java SE from the Oracle website at
www.oracle.com/technetwork/java/javase/downloads/index.html.
Play also works with Open JDK version 1.8 and above. But you should make sure that any dependency
you use in the project is compatible with the Open JDK.
Installing sbt
sbt (Scala build tool) is available for all OS versions at the scala-sbt website at www.scala-
sbt.org/download.html. Please follow the instructions in the installation guide at www.scala-
sbt.org/1.x/docs/Setup.html to install sbt.
Installing conscript
To install Play using sbt, you need to install conscript. Please follow the instructions at
www.foundweekends.org/conscript/setup.html to install conscript. The instructions are
available for Mac, Linux, and Windows.
The cross-platform installation using a jar is simple. Download the conscript jar from the
foundweekends Maven release repo (https://dl.bintray.com/foundweekends/maven-
releases/org/foundweekends/conscript/conscript_2.11/0.5.2/conscript_2.11-
0.5.2-proguard.jar) and run the following from the command prompt:
This will start a splash screen and will install conscript. Ignore any error related to not finding ‘cs.’
Typically, conscript will get installed in the C:\Users\username\.conscript folder in Windows,
where username is the home folder of the logged-in user in Windows. This location will be shown in the
splash screen.
Now let’s set up the environment variables for conscript:
CONSCRIPT_HOME is where conscript will download various files. For example, in Windows, it will
be C:\Users\username\.
PATH is your OS’s path variable. This will make the command cs available from everywhere.
Installing Giter8
After installing conscript, Giter8 can be installed using conscript itself. Giter8 is a command-line tool to
generate files and directories from templates published on GitHub or any other git repository.
Go to a command prompt and type
cs foundweekends/giter8
This will download and install Giter8. More details of the installation is available from the
foundweekends website for Giter8 (www.foundweekends.org/giter8/setup.html).
Setting Up Play
There are multiple ways to install Play. Let’s look at the two most common ways:
Using Play example projects: Use any of the example projects provided by Lightbend Tech Hub
(https://developer.lightbend.com/start/?group=play).
Using sbt: This approach depends on Giter8 templates for sbt. This method creates a fresh Play
project without any example code. This is the preferred mode for a clean, lean project structure.
3. Click the “Create a project for me” button (this will download the example project to your system).
Note that the last part of the message (f50b441db70fa47676ba) might be different for you
but that is okay. You are now good to go and you will see the prompt as
[play-scala-seed] $
[play-scala-seed] $ run
7. Go to http://localhost:9000. If all is good, you should see the Play documentation displayed
in the browser.
Now let’s see how to install a Play example project using Java.
1. Go to https://developer.lightbend.com/start/?group=play.
3. Click the “Create a project for me” button (this will download the example project to your system).
6. This will download all the dependencies and perform the build. Please note that it might take a while
to download all the dependencies.
[play-java-hello-world-tutorial] $
[play-java-hello-world-tutorial] $ run
Using sbt
Now you’ll learn how to install Play using sbt to get the basic Play application structure and dependent
jars but not the example projects.
To use sbt to create a Play project, the following needs to be completed:
1. Install SBT.
2. Install conscript.
3. Install Giter8.
You can find instructions for installing the above software in the Installation section of this chapter.
Once the above software is installed, you can proceed with creating a Play project using sbt. Play
supports Java and Scala as programming languages, so I will show examples for both using sbt.
Provide the project name and organization, and the project will get created.
You have now learned multiple ways to install a Play project for both Scala and Java:
Using Play examples
Using sbt
In this book, I use Java as the programming language for Play, so when you create your first project (a
bookshop) in the next section, you will be using sbt and the Java seed.
For the organization name, you can give another domain name if you want to do so.
Go to the directory where you created project. For instance, in my machine, the project directory is
E:\workarea\bookshop.
Let’s look at the folder structure and its relevance in the overall project organization. See Figure 1-2.
app
The app folder contains all your server-side source files. This includes all your Java code, Scala code,
dynamic Scala HTML templates, database access-related code, etc. By default, Play creates two folders,
controllers and views, inside the app folder. The names are self-explanatory. The controllers
folder holds your controller classes and views holds the dynamic screens (the HTML and Scala code
snippets).
You are free to create subdirectories inside the app folder for better organization of your files. For
example, you can create a models folder to hold all your ORM mapped POJOs, a folder named helper to
hold your helper classes, etc.
Most of the project will have the following structure under the app folder:
app
└ assets: Compiled asset sources
└ stylesheets: For CSS source code (less CSS sources)
└ javascripts: Typically this is the folder where coffeescript sources are placed.
└ controllers: Application controllers
└ models: Application business layer
└ views: Templates
conf
The conf folder holds the configurations used by the Play application. It contains all the HTTP
mappings, orm configurations, environmental variables, logging, etc.
Basically, the conf directory contains configuration and internationalization files, whereas the app
folder has a subdirectory for its model definitions.
The most important files in this directory are
application.conf: The main configuration file for the application, it contains standard
configuration parameters.
routes: Maps HTTP URL paths to methods in the controller. Handles all HTTP routing configurations.
logback: Play uses logback for all logging configuration and this is the file you need to change to
configure logback.
build.sbt
Play’s build configuration is defined in two places: the build.sbt file in the project root and two files
found inside the /project folder. The build.sbt files contain the build configuration.
project
The project folder contains project build configurations:
plugins.sbt: Defines the sbt plugins used by this project
build.properties: Contains the sbt versions to use to build your app and related sbt build
information.
I will discuss more about sbt going forward. A basic understanding of sbt is good when you work
with Play.
public
The public folder hosts all static files like Javascripts, images, and CSS style sheets that are directly
served by the web server. The public folder has three subfolders, images, javascripts, and
stylesheets, for storing these assets:
public
└ stylesheets: CSS files (.css extension)
└ javascripts: JavaScript files (.js files)
└ images: Images
lib
The lib folder is not created by default. But you can create this folder and put any jar into it. All jars in
this folder will be added to the application class path. It’s ideal for including third-party dependencies
that need to be managed out of the Play build system.
test
The test folder is a holder for storing all unit and functional test cases.
Setting Up in Eclipse
As an example, let’s configure Play for Eclipse. To use Play with Eclipse, you need to first integrate
sbteclipse to your project. To do this, open plugins.sbt (project/plugins.sbt) and add the
following:
You want to compile the project before you run the eclipse command to generate the eclipse import
settings for the bookshop project. The manual way is to run sbt compile first and then do the
generate eclipse project part. But you can do it in a better and automated way; you can instruct it to run
compilation first whenever you generate eclipse project settings. For this, open the build.sbt file and
add the following:
Please note the above is only for Java projects. If you have Scala sources, then you should use Scala
IDE instead of the regular Eclipse IDE.
Save the build.sbt file and go to the sbt prompt by taking a command prompt, moving to the
project root directory (E:\workarea\bookshop) and typing sbt.
This initializes the sbt prompt. This can take few minutes to complete because sbt will download the
plugins and all related dependencies. Once the sbt prompt gets initialized, you should see the prompt as
[bookshop] $
Type eclipse with-source=false and press Enter. If you need all the source jars of the
dependencies, you can issue eclipse with-source=true instead.
After the successful execution of the above command, start eclipse and import the project. See Figure
1-3.
Figure 1-3 Import Wizard
1. Open Eclipse.
4. Click Finish.
Setting Up in IntelliJ
Importing a Play project to Intellij is pretty straightforward. The only precondition is that the Scala
plugin for Intellij should be installed, even when using Java as the language. This is because the Scala
plugin for Intellij is required to resolve the sbt dependencies. So go ahead and install the Scala plugin for
Intellij if you have not done so. Open Intellij and go to File ➤ Settings ➤ Plugins and search for Scala in
the Marketplace tab. From the plugins listed, select the Scala plugin from JetBrains and click Install. See
Figure 1-4.
Figure 1-4 Importing the Scala plugin
After installing the Scala plugin, import the bookshop Play project to IntelliJ. See Figure 1-5.
4. Choose sbt.
5. Click Finish.
Wait for the build to sync and you will see that the project is imported to Intellij.
(If the server started, use Enter to stop, and go back to the console.)
You can even combine the commands together to start Play by using
sbt run
Figure 1-6 Application console
Open the browser and access http://localhost:9000/ to access the home page. See Figure 1-
7.
Configuration
Go to the conf folder and open the routes file. This is where all the URL mappings of the project need
to be defined. You will examine URL mappings and routes configuration in detail in the next chapter.
When you open up the routes file, you should see an entry like this:
GET / controllers.HomeController.index
This means the root of the application points to the index method defined in the
HomeController class.
If you type localhost:portnumber in the browser, the request will be routed to the index
method defined in the app/controllers/HomeController.java file.
This is how Play routes the URL paths or URL patterns to the specific methods of the controller
classes.
Open the HomeController.java file and you can see the method
This method returns a Twirl template file and this template file generates the HTML output. Open the
index.scala.html file found inside the views folder. Let’s examine the contents of this file to
understand what is happening in it.
@()
@main("Welcome to Play") {
<h1>Welcome to Play!</h1>
}
Before we take a look at each element, it is important to understand what Twirl is and how it can be
used.
What is Twirl?
Twirl is the template engine developed for Play Framework. But it can also be used outside the Play
environment. By default, Twirl is included as part of Play but if there is a need to use Twirl outside
Play, then the sbt plugin for Scala can be installed. For example adding the below entry in the
plugins.sbt file will make Twirl available to any sbt-based project:
Template files must be named {name}.scala.{ext} where ext can be html, js, xml, or txt.
The templates can be used to generate various types of markup like HTML, XML, or TXT and are
totally decoupled from the controller. Various kinds of markup can be plugged in as needed.
The Twirl template is just a normal text file that contains small blocks of Scala code. Templates
help to create composite views and help in a component-based view generation.
The @ character marks the beginning of the dynamic code in the template. Chapter 4 of this book
provides detailed explanation of views and Twirl templates.
For the time being, let’s understand what is defined in the index.scala.html file:
@main("Welcome to Play") {
<h1>Welcome to Play!</h1>
}
@main("Welcome to Play") calls another template, main.scala.html and passes it the page
title “Welcome to Play” and the HTML content in the second parameter, enclosed within the {}.
Hence you can infer that main template file should take two parameters: a string for the title and
HTML content as the second parameter.
Open the main.scala.html to validate this. The file starts with @(title: String)
(content: Html); this is just like any normal method that accepts two parameters.
@*
* This template is called from the `index` template. This template
* handles the rendering of the page header and body tags. It takes
* two arguments, a `String` for the title of the page and an `Html`
* object to insert into the body of the page.
*@
<!DOCTYPE html>
<html lang="en">
<head>
@* Here's where we render the page title `String`. *@
<title>@title</title>
<link rel="stylesheet" media="screen"
href="@routes.Assets.versioned("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png"
href="@routes.Assets.versioned("images/favicon.png")">
</head>
<body>
@* And here's where we render the `Html` object containing
* the page content. *@
@content
<script src="@routes.Assets.versioned("javascripts/main.js")"
type="text/javascript"></script>
</body>
</html>
The title string is inserted to HTML <title> using @title and HTML content using @content
markup.
Now you know the different elements and how they are wired together. Let’s take this further by
writing a new action and a view.
Let’s create an entry for the Hello World method:
Save the routes file. The next step is to code your controller to handle the request.
package controllers;
import play.mvc.*;
import views.html.*;
import java.time.LocalDate;
/**
* This controller contains an action to handle HTTP requests
* to the application's home page.
*/
public class HomeController extends Controller {
/**
* An action that renders an HTML page with a welcome message.
* The configuration in the <code>routes</code> file means that
* this method will be called when the application receives a
* <code>GET</code> request with a path of <code>/</code>.
*/
public Result index() {
return ok(views.html.index.render());
}
}
}
Let’s examine the HomeController class in detail to understand what is happening in it. First of
all, it extends from play.mvc.Controller. When you write a new controller, make sure you extend
from play.mvc.Controller.
You’ve seen the index method before: it is pretty simple and it just has a single line. But it does a lot
of smart things. The ok() method is closely related to HTTP status 200, or the success response. If you
want to return a HTTP not found, you can use the notFound method. This is the beauty of Play; it
closely resembles HTTP protocol and there is no need for any fancy code to convert your exceptions to
corresponding HTTP status codes. You can code and talk the language of HTTP.
The ok() method returns the HTML output generated by the render method, defined in the view
named index. You’ve seen this view in index.scala.html. Views in Play use Scala and follow the
naming convention of “viewname.scala.html”. This file gets compiled by the Play compiler into the
corresponding Java class, which can be directly used in the controller. This ensures type safety and less
bugs. Remember that in other frameworks like struts or Spring MVC, you typically return a string as view
name and the framework resolves that to a file. In Play, there is no need for that. The views are
straightaway available as Java class files and you can ensure compile-time type safety.
Since you have already added the routes entry for the hello method, let’s proceed with adding the
controller method and create the view.
View
Create a new file inside the app/views folder and name it hello.scala.html and add the following
contents:
@(message: String)
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello World </title>
</head>
<body>
<h1> @message </h1>
</body>
</html>
This template takes a single parameter and places that inside the HTML <h1> tags.
Controller
Now edit the HomeController:
package controllers;
import play.mvc.*;
import views.html.*;
import java.time.LocalDate;
/**
* This controller contains an action to handle HTTP requests
* to the application's home page.
*/
public class HomeController extends Controller {
/**
* An action that renders an HTML page with a welcome message.
* The configuration in the <code>routes</code> file means that
* this method will be called when the application receives a
* <code>GET</code> request with a path of <code>/</code>.
*/
public Result index() {
return ok(views.html.index.render());
}
public Result hello() {
return ok(views.html.hello.render("Today is "+LocalDate.now()));
}
}
http://localhost:9000/hello
You will see the message “Today is” and the current date. See Figure 1-8.
Just change any HTML code in the view and refresh the browser. Play will perform on-the-fly
compilation and render the view.
body {
background: black;
}
h1,h2 {
color: white;
}
@(message: String)
@main("Hello World") {
<h2>@message</h2>
}
You want a common theme across the web application and all pages should inherit the site wide
settings as is. You don’t want to scatter the site-wide definitions across all pages, so put all common
settings like header, footer, style sheet definitions, JavaScript inclusions, etc. in a single file; that is what
main.scala.html is used for. It defines the common elements applicable to all pages.
The main.scala.html file is converted by Play into a method equivalent and can be invoked from
other pages using its name. The name doesn’t include the .scala.html part. For instance,
Random documents with unrelated
content Scribd suggests to you:
XXII
Mitä hän välitti siitä, että ilkeät kielet parjasivat häntä Lahja
Kaarion tähden. Olihan siitä puhuttu niin, että jutut olivat päässeet
hänenkin korviinsa. Itse hän parhaiten tiesi, kuinka paljon oli
kärsinyt tuon onnettoman suhteen takia. Ja täytyihän niiden juttujen
kerran vaieta, kun neiti Kaario nyt oli onnellisesti kihloissa. Mutta
enemmän koski häneen sittenkin tunto siitä, että opiskeleva nuoriso,
varsinkin osakunnassa, oli ymmärtänyt häntä väärin, pitäen häntä
ankarana tuomarina, joka itseltänsä vaati vähän, mutta muilta sitä
enemmän…
*****
Updated editions will replace the previous one—the old editions will
be renamed.
1.D. The copyright laws of the place where you are located also
govern what you can do with this work. Copyright laws in most
countries are in a constant state of change. If you are outside the
United States, check the laws of your country in addition to the
terms of this agreement before downloading, copying, displaying,
performing, distributing or creating derivative works based on this
work or any other Project Gutenberg™ work. The Foundation makes
no representations concerning the copyright status of any work in
any country other than the United States.
1.E.6. You may convert to and distribute this work in any binary,
compressed, marked up, nonproprietary or proprietary form,
including any word processing or hypertext form. However, if you
provide access to or distribute copies of a Project Gutenberg™ work
in a format other than “Plain Vanilla ASCII” or other format used in
the official version posted on the official Project Gutenberg™ website
(www.gutenberg.org), you must, at no additional cost, fee or
expense to the user, provide a copy, a means of exporting a copy, or
a means of obtaining a copy upon request, of the work in its original
“Plain Vanilla ASCII” or other form. Any alternate format must
include the full Project Gutenberg™ License as specified in
paragraph 1.E.1.
• You pay a royalty fee of 20% of the gross profits you derive
from the use of Project Gutenberg™ works calculated using the
method you already use to calculate your applicable taxes. The
fee is owed to the owner of the Project Gutenberg™ trademark,
but he has agreed to donate royalties under this paragraph to
the Project Gutenberg Literary Archive Foundation. Royalty
payments must be paid within 60 days following each date on
which you prepare (or are legally required to prepare) your
periodic tax returns. Royalty payments should be clearly marked
as such and sent to the Project Gutenberg Literary Archive
Foundation at the address specified in Section 4, “Information
about donations to the Project Gutenberg Literary Archive
Foundation.”
• You comply with all other terms of this agreement for free
distribution of Project Gutenberg™ works.
1.F.
1.F.4. Except for the limited right of replacement or refund set forth
in paragraph 1.F.3, this work is provided to you ‘AS-IS’, WITH NO
OTHER WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO WARRANTIES OF
MERCHANTABILITY OR FITNESS FOR ANY PURPOSE.
Please check the Project Gutenberg web pages for current donation
methods and addresses. Donations are accepted in a number of
other ways including checks, online payments and credit card
donations. To donate, please visit: www.gutenberg.org/donate.
Most people start at our website which has the main PG search
facility: www.gutenberg.org.
Our website is not just a platform for buying books, but a bridge
connecting readers to the timeless values of culture and wisdom. With
an elegant, user-friendly interface and an intelligent search system,
we are committed to providing a quick and convenient shopping
experience. Additionally, our special promotions and home delivery
services ensure that you save time and fully enjoy the joy of reading.
textbookfull.com