简介

本节介绍如何使用 Grails 和 Taack UI 框架在您的图书馆管理应用程序中生成数据并实现不同类型的图表。

环境准备

本任务的完整解答与骨架代码可从以下仓库克隆:

git clone git@github.com:Taack/myLibrary.git
要获取本部分的骨架代码,请打开终端并 cd 进入项目目录(myLibrary),然后复制并执行以下命令。
git checkout part-4-skeleton

或者在 IntelliJ 中直接选择所需的分支。点击左上角的 main/myLibrary/remote 即可查看所有分支,选择对应分支并执行 checkout。

创建数据

我们需要来自用户的数据来创建图表。这些数据通过 Grails 领域类和服务生成,以创建逼真的用户、作者、图书和借阅记录条目。如需了解更多信息,请参阅 GORM 官方文档。

您也可以查看新添加的文件:app/myLibrary/grails-app/controllers/my/library/MyLibraryDataController.groovy。

要创建数据,只需在浏览器地址栏中访问 /myLibraryData/createDataUsers。请注意,这不会清除之前已创建的数据;如果数据已经创建过,也不会重复创建。

创建图表

示例代码

要创建图表,您将使用 UiDiagramSpecifier()。框架提供了多种不同的图表选项(柱状图、折线图、饼图、箱线图/whiskers、面积图)。要查看所有这些图表的实现方式,请参阅 图表 DSL(Diagram DSL)。

在本示例中,我们希望可视化馆藏图书数量最多的前 10 位作者各自的图书数量。

首先,我们需要从 MyLibraryBook 中检索相关信息。

HQL 代码(示例代码):
List<Object[]> results = MyLibraryBorrowed.executeQuery("""\
    select book.author, count(book)
    from MyLibraryBook book
    group by book.author
     """, [max: 10])

在这里,我们保存了前 10 位作者各自的姓氏(lastName)及其对应的图书数量。

现在,我们需要将这些信息整理为 DiagramSpecifier 可读取的格式。

示例代码:
Map<String, BigDecimal> authorCounts = [:]
results.each { row ->
    String lastName = row[0] as String
    Long count = row[1] as Long
    authorCounts[lastName] = count.toBigDecimal()
}

现在我们可以创建饼图了。

饼图(示例代码):
new UiDiagramSpecifier().ui {
    pie(hasSlice, {
        List<String> authorsSorted = authorCounts.keySet().toList()
        List<BigDecimal> counts = authorsSorted.collect { authorCounts[it] }

        // For pie, each dataset entry is a separate slice with its label
        labels "Author Pie"
        (0..<authorsSorted.size()).each { i ->
            dataset(authorsSorted[i], counts[i])
        }
    })
}

这段代码以及 Diagram 菜单已经为您在 MyLibraryUiService 中实现好了。为了方便起见,我们也已经开始了 listDiagram 的实现,请查看 MyLibraryController。

您可以运行服务器并导航到 Diagram 菜单,查看示例饼图。 接下来,让我们创建自己的图表。

Reload
Figure 1. 图表

饼图(Pie Diagram)

我们希望能够直观地看到哪些图书最受借阅者欢迎。为此,我们决定创建一个饼图,用于展示借阅次数最多的前 10 本图书。

实现代码
UiDiagramSpecifier buildBookPopularityPieDiagram(boolean hasSlice) {
    Map<String, BigDecimal> bookCounts = [:]

    List<Object[]> results = MyLibraryBorrowed.executeQuery("""\
        select bi.book.title, count(borrowed)
        from MyLibraryBorrowed borrowed
        join borrowed.bookInstance bi
        group by bi.book.title
        order by count(borrowed) desc
         """, [max: 10])

    results.each { row ->
        String title = row[0] as String
        Long count = row[1] as Long
        bookCounts[title] = count.toBigDecimal()
    }

    new UiDiagramSpecifier().ui {
        pie(hasSlice, {
            List<String> titlesSorted = bookCounts.keySet().toList()
            List<BigDecimal> counts = titlesSorted.collect { bookCounts[it] }

            labels "Pie"
            (0..<titlesSorted.size()).each { i ->
                dataset(titlesSorted[i], counts[i])
            }
        })
    }
}
渲染图表
UiDiagramSpecifier diagramPieSpec = myLibraryUiService.buildBookPopularityPieDiagram(true) // TODO 1.2.1

diagram diagramPieSpec, {label "Book Popularity Pie"} // TODO 1.2.2

现在重新运行服务器并导航到 Diagram 菜单,即可看到新创建的饼图。

Reload
Figure 2. 图表

柱状图(Bar Diagram)

我们希望可视化借阅者申请借阅的图书数量。为此,我们决定创建一个柱状图,按年、月、日展示借阅申请的数量。

实现代码
UiDiagramSpecifier barDiagram(boolean isStacked) {
    new UiDiagramSpecifier().ui {
        bar(isStacked, {
            labels "T1", "T2", "T3", "T4"
            dataset 'Truc1', 1.0, 2.0, 1.0, 4.0
            dataset 'Truc2', 2.0, 0.1, 1.0, 0.0
            dataset 'Truc3', 2.0, 0.1, 1.0, 1.0
        })
    }
}
实现 buildBarDiagram:
UiDiagramSpecifier buildBarDiagram(boolean isStacked, String labelDateFormat) {
    Map<Date, BigDecimal> borrowedCounts = [:]

    List<Object[]> results1 = MyLibraryBorrowed.executeQuery("""\
        select count(borrowed), borrowed.requestDate
        from MyLibraryBorrowed borrowed
        group by borrowed.requestDate
        """)

    results1.each { row ->
        Long count = row[0] as Long
        Date date = row[1] as Date
        borrowedCounts[date] = (borrowedCounts[date] ?: 0) + count.toBigDecimal()
    }

    new UiDiagramSpecifier().ui {
        bar(isStacked, {
            List<Map.Entry<Date, BigDecimal>> countsSorted = borrowedCounts.entrySet().sort { it.key }.collect { it }

            labels((labelDateFormat ?: 'YEAR') as DiagramXLabelDateFormat, countsSorted*.key as Date[]) //(1)
            dataset('Borrowed Books', countsSorted*.value as BigDecimal[])
        })
    }
}

默认情况下,图表将按年份显示。

渲染图表
UiDiagramSpecifier diagramBarSpec = myLibraryUiService.buildBarDiagram(true, labelDateFormat) // TODO 2.2.1

// TODO 2.2.2
diagram(diagramBarSpec, {
    menu 'Yearly', this.&listDiagrams as MethodClosure, [labelDateFormat: 'YEAR']
    menu 'Monthly', this.&listDiagrams as MethodClosure, [labelDateFormat: 'MONTH']
    menu 'Daily', this.&listDiagrams as MethodClosure, [labelDateFormat: 'DAY']
})

现在您可以再次运行服务器,查看新创建的柱状图。

Reload
Figure 3. 图表

箱线图(Whiskers Diagram)

我们希望能够可视化借阅者借阅图书的天数分布。为此,我们决定创建一个箱线图(Whiskers 图),展示在各个指定天数区间(1–3 天、4–7 天、8–14 天、15–20 天以及 21 天以上)内借阅图书的时长分布。

实现代码
diagram new UiDiagramSpecifier().ui({
    whiskers {
        labels "T1", "T2", "T3", "T4"

        dataset 'Truc1', {
            boxData 1.0, 2.0, 3.0, 3.5, 4.0, 4.5, 5.0
            boxData 1.5, 2.5, 3.5, 6.0, 7.0, 8.0, 9.0
            boxData 2.0, 2.0, 2.1, 2.5, 5.5, 5.6, 6.7
            boxData 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3
        }
    }
})
实现 buildBorrowDurationWhiskersDiagram:
UiDiagramSpecifier buildBorrowDurationWhiskersDiagram() {
    Map<Integer, BigDecimal> borrowedDates = [:]
    Map<String, List<BigDecimal>> bins = [
            "1-3d"  : [],
            "4-7d"  : [],
            "8-14d" : [],
            "15-20d": [],
            "21+d"  : []
    ]

    List<Object[]> results1 = MyLibraryBorrowed.executeQuery("""\
        select borrowed.approvalDate, borrowed.returnDate
        from MyLibraryBorrowed borrowed
        where borrowed.approvalDate is not null and borrowed.returnDate is not null
""")

    results1.each { row ->
        Date approvalDate = row[0] as Date
        Date returnDate = row[1] as Date

        long durationMillis = returnDate.time - approvalDate.time
        int duration = (durationMillis / (1000 * 60 * 60 * 24)).toInteger()
        borrowedDates[duration] = (borrowedDates[duration] ?: 0.toBigDecimal()) + 1.toBigDecimal()
        BigDecimal durationBD = duration as BigDecimal

        if (duration <= 3) bins["1-3d"] << durationBD
        else if (duration <= 7) bins["4-7d"] << durationBD
        else if (duration <= 14) bins["8-14d"] << durationBD
        else if (duration <= 20) bins["15-20d"] << durationBD
        else bins["21+d"] << durationBD
    }

    new UiDiagramSpecifier().ui {
        whiskers {
            labels bins.keySet() as String[]

            dataset('Borrow Durations', {
                bins.each { label, values ->
                    if (values) {
                        List<BigDecimal> sorted = values.sort()
                        // min, Q1, median, Q3, max + dummy outliers if needed
                        boxData sorted[0], sorted[(int)(sorted.size()*0.25)], sorted[(int)(sorted.size()*0.5)],
                                sorted[(int)(sorted.size()*0.75)], sorted[-1],
                                sorted[0], sorted[-1]
                    } else {
                        boxData 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
                    }
                }
            })
        }
    }
}
渲染图表
UiDiagramSpecifier durationDiagramSpec = myLibraryUiService.buildBorrowDurationWhiskersDiagram() // TODO 3.2.1

diagram durationDiagramSpec // TODO 3.2.2

现在再次运行服务器,即可查看新创建的箱线图。

Reload
Figure 4. 图表
参考解答
package sid.books

import crew.Role
import crew.User
import crew.UserRole
import grails.compiler.GrailsCompileStatic
import grails.gorm.transactions.Transactional
import grails.plugin.springsecurity.annotation.Secured

import java.text.SimpleDateFormat
import java.time.LocalDate
import java.time.ZoneId

@Secured(['ROLE_ADMIN'])
@GrailsCompileStatic
class GraphsController {
    Random random = new Random(42)

    def index() {
        redirect action: 'createDataUsers'
    }

    @Secured(['ROLE_ADMIN'])
    @Transactional
    def createDataUsers() {
        Role borrowerRole = Role.findByAuthority('ROLE_BORROWER') ?: new Role(authority: "ROLE_BORROWER")
        borrowerRole.save(flush: true)
        Role adminRole = Role.findByAuthority('ROLE_ADMIN') ?: new Role(authority: "ROLE_ADMIN")
        adminRole.save(flush: true)
        List<Map> adminUsers = [
                [firstName: 'John', lastName: 'Doe', username: 'jdoe'],
                [firstName: 'Alice', lastName: 'Smith', username: 'asmith'],
                [firstName: 'Brian', lastName: 'Williams', username: 'bwilliams']
        ]

        List<Map> borrowerUsers = [
                [firstName: 'Emily', lastName: 'Johnson', username: 'ejohnson'], [firstName: 'Michael', lastName: 'Brown', username: 'mbrown'],
                [firstName: 'Jessica', lastName: 'Davis', username: 'jdavis'], [firstName: 'David', lastName: 'Miller', username: 'dmiller'],
                [firstName: 'Sarah', lastName: 'Wilson', username: 'swilson'], [firstName: 'Daniel', lastName: 'Moore', username: 'dmoore'],
                [firstName: 'Laura', lastName: 'Taylor', username: 'ltaylor'], [firstName: 'James', lastName: 'Anderson', username: 'janderson'],
                [firstName: 'Olivia', lastName: 'Thomas', username: 'othomas'], [firstName: 'Matthew', lastName: 'Jackson', username: 'mjackson'],
                [firstName: 'Sophia', lastName: 'White', username: 'swhite'], [firstName: 'Anthony', lastName: 'Harris', username: 'aharris'],
                [firstName: 'Isabella', lastName: 'Martin', username: 'imartin'], [firstName: 'Andrew', lastName: 'Thompson', username: 'athompson'],
                [firstName: 'Megan', lastName: 'Garcia', username: 'mgarcia'], [firstName: 'Joshua', lastName: 'Martinez', username: 'jmartinez'],
                [firstName: 'Grace', lastName: 'Robinson', username: 'grobinson'], [firstName: 'Christopher', lastName: 'Clark', username: 'cclark'],
                [firstName: 'Natalie', lastName: 'Rodriguez', username: 'nrodriguez'], [firstName: 'Brandon', lastName: 'Lewis', username: 'blewis'],
                [firstName: 'Ashley', lastName: 'Walker', username: 'awalker'], [firstName: 'Ryan', lastName: 'Hall', username: 'rhall'],
                [firstName: 'Abigail', lastName: 'Allen', username: 'aallen'], [firstName: 'Justin', lastName: 'Young', username: 'jyoung'],
                [firstName: 'Samantha', lastName: 'Hernandez', username: 'shernandez'], [firstName: 'Jacob', lastName: 'King', username: 'jking'],
                [firstName: 'Madison', lastName: 'Wright', username: 'mwright'], [firstName: 'Ethan', lastName: 'Lopez', username: 'elopez'],
                [firstName: 'Elizabeth', lastName: 'Hill', username: 'ehill'], [firstName: 'Alexander', lastName: 'Scott', username: 'ascott'],
                [firstName: 'Victoria', lastName: 'Green', username: 'vgreen'], [firstName: 'Benjamin', lastName: 'Adams', username: 'badams'],
                [firstName: 'Chloe', lastName: 'Baker', username: 'cbaker'], [firstName: 'William', lastName: 'Nelson', username: 'wnelson'],
                [firstName: 'Ella', lastName: 'Carter', username: 'ecarter'], [firstName: 'Joseph', lastName: 'Mitchell', username: 'jmitchell'],
                [firstName: 'Mia', lastName: 'Perez', username: 'mperez'], [firstName: 'Noah', lastName: 'Roberts', username: 'nroberts'],
                [firstName: 'Lily', lastName: 'Turner', username: 'lturner'], [firstName: 'Logan', lastName: 'Phillips', username: 'lphillips'],
                [firstName: 'Zoe', lastName: 'Campbell', username: 'zcampbell'], [firstName: 'Jayden', lastName: 'Parker', username: 'jparker'],
                [firstName: 'Hannah', lastName: 'Evans', username: 'hevans'], [firstName: 'Lucas', lastName: 'Edwards', username: 'ledwards'],
                [firstName: 'Avery', lastName: 'Collins', username: 'acollins'], [firstName: 'Mason', lastName: 'Stewart', username: 'mstewart'],
                [firstName: 'Scarlett', lastName: 'Sanchez', username: 'ssanchez'], [firstName: 'Jack', lastName: 'Morris', username: 'jmorris'],
                [firstName: 'Aria', lastName: 'Rogers', username: 'arogers'], [firstName: 'Henry', lastName: 'Reed', username: 'hreed'],
                [firstName: 'Amelia', lastName: 'Cook', username: 'acook'], [firstName: 'Sebastian', lastName: 'Morgan', username: 'smorgan'],
                [firstName: 'Layla', lastName: 'Bell', username: 'lbell'], [firstName: 'Owen', lastName: 'Murphy', username: 'omurphy'],
                [firstName: 'Ella', lastName: 'Bailey', username: 'ebailey'], [firstName: 'Gabriel', lastName: 'Rivera', username: 'grivera'],
                [firstName: 'Aubrey', lastName: 'Cooper', username: 'acooper'], [firstName: 'Carter', lastName: 'Richardson', username: 'crichardson'],
                [firstName: 'Sofia', lastName: 'Cox', username: 'scox'], [firstName: 'Wyatt', lastName: 'Howard', username: 'whoward'],
                [firstName: 'Riley', lastName: 'Ward', username: 'rward'], [firstName: 'Dylan', lastName: 'Torres', username: 'dtorres'],
                [firstName: 'Camila', lastName: 'Peterson', username: 'cpeterson'], [firstName: 'Nathan', lastName: 'Gray', username: 'ngray'],
                [firstName: 'Leah', lastName: 'Ramirez', username: 'lramirez'], [firstName: 'Elijah', lastName: 'James', username: 'ejames'],
                [firstName: 'Hazel', lastName: 'Watson', username: 'hwatson'], [firstName: 'Isaac', lastName: 'Brooks', username: 'ibrooks'],
                [firstName: 'Lillian', lastName: 'Sanders', username: 'lsanders'], [firstName: 'Matthew', lastName: 'Price', username: 'mprice']
        ]

        def allUsers = adminUsers.collect {it -> it + [role: 'ADMIN'] } +
                borrowerUsers.collect { it -> it + [role: 'BORROWER'] }
        allUsers.each {
            item ->
                {
                    User user = User.findByUsername(item.username as String)
                    if (!user) {
                        user = new User(
                                username: item.username,
                                password: '{noop}123',
                                businessUnit: 'IT',
                                firstName: item.firstName,
                                lastName: item.lastName
                        )
                        user.save(flush: true)
                        println "Created ${item.role} user: ${user.username} with errors:=${user.errors}"
                    } else {
                        println "Admin user: ${user.username} already created"
                    }
                    UserRole userRole = null
                    if (item.role == "ADMIN") {
                        if (!UserRole.findByUserAndRole(user, adminRole)) {
                            userRole = new UserRole(user: user, role: adminRole)
                        }
                    } else {
                        if (!UserRole.findByUserAndRole(user, borrowerRole)) {
                            userRole = new UserRole(user: user, role: borrowerRole)
                        }
                    }
                    if (userRole) {
                        userRole?.save(flush: true)
                        println "Linked ${item.username} to ROLE_${item.role}"
                    }
                }
        }
        createDataAuthorsBooks()
    }


    @Transactional
    def createDataAuthorsBooks() {

        List<Map> authorData = [
                [firstName: 'Emily', lastName: 'Stone', dob: '1975-04-12'],
                [firstName: 'Michael', lastName: 'King', dob: '1968-11-03'],
                [firstName: 'Olivia', lastName: 'Reed', dob: '1982-06-25'],
                [firstName: 'Daniel', lastName: 'Bennett', dob: '1959-09-17'],
                [firstName: 'Sophia', lastName: 'Ward', dob: '1970-12-29'],
                [firstName: 'James', lastName: 'Brooks', dob: '1965-03-05'],
                [firstName: 'Isabella', lastName: 'Gray', dob: '1987-10-14'],
                [firstName: 'Matthew', lastName: 'Kelly', dob: '1978-07-21'],
                [firstName: 'Mia', lastName: 'Cook', dob: '1990-02-09'],
                [firstName: 'Alexander', lastName: 'Murphy', dob: '1963-01-31'],
                [firstName: 'Charlotte', lastName: 'Bailey', dob: '1984-05-16'],
                [firstName: 'David', lastName: 'Cooper', dob: '1955-08-27'],
                [firstName: 'Amelia', lastName: 'Rivera', dob: '1981-04-03'],
                [firstName: 'Benjamin', lastName: 'Richardson', dob: '1973-09-12'],
                [firstName: 'Evelyn', lastName: 'Cox', dob: '1969-06-18'],
                [firstName: 'Lucas', lastName: 'Howard', dob: '1976-02-22'],
                [firstName: 'Harper', lastName: 'Ward', dob: '1985-10-30'],
                [firstName: 'Henry', lastName: 'Torres', dob: '1960-07-04'],
                [firstName: 'Ella', lastName: 'Peterson', dob: '1989-03-27'],
                [firstName: 'Jack', lastName: 'Gray', dob: '1974-12-08'],
        ]


        authorData.each { data ->
            Author author = Author.findByFirstNameAndLastName(data.firstName as String, data.lastName as String)
            if (!author) {
                author = new Author(
                        firstName: data.firstName,
                        lastName: data.lastName,
                        dateOfBirth: new SimpleDateFormat('yyyy-MM-dd').parse(data.dob as String),
                        listOfBooks: []
                )
                author.save(flush: true)
                println "Created author: ${author.firstName} ${author.lastName} with errors:=${author.errors}"
            } else {
                println "Author: ${author.firstName} ${author.lastName} already created"
            }

        }

        List<Map> booksData = [
                [title: "The Silent Patient", description: "A thrilling psychological mystery novel.", numberOfPages: "336"],
                [title: "Educated: A Memoir", description: "An inspiring journey through education and resilience.", numberOfPages: "352"],
                [title: "Becoming", description: "A powerful memoir exploring identity and purpose.", numberOfPages: "448"],
                [title: "Where the Crawdads Sing", description: "A captivating blend of mystery and nature writing.", numberOfPages: "384"],
                [title: "Normal People", description: "A complex love story set in Ireland.", numberOfPages: "288"],
                [title: "Atomic Habits", description: "A comprehensive guide to building good habits and breaking bad ones.", numberOfPages: "320"],
                [title: "The Subtle Art of Not Giving a F*ck", description: "A brutally honest self-help book.", numberOfPages: "224"],
                [title: "Thinking, Fast and Slow", description: "An in-depth analysis of human decision-making and psychology.", numberOfPages: "512"],
                [title: "Sapiens: A Brief History of Humankind", description: "A sweeping exploration of human history.", numberOfPages: "464"],
                [title: "Dune", description: "A classic science fiction epic.", numberOfPages: "896"],
                [title: "The Great Gatsby", description: "A timeless novel of wealth, love, and loss.", numberOfPages: "180"],
                [title: "1984", description: "A chilling dystopian classic about surveillance and control.", numberOfPages: "328"],
                [title: "To Kill a Mockingbird", description: "A powerful novel about justice and morality.", numberOfPages: "336"],
                [title: "The Catcher in the Rye", description: "A coming-of-age story of teenage alienation.", numberOfPages: "277"],
                [title: "Brave New World", description: "A dystopian vision of a technologically controlled society.", numberOfPages: "311"],
                [title: "The Alchemist", description: "A philosophical novel about destiny and dreams.", numberOfPages: "208"],
                [title: "The Road", description: "A bleak and beautiful post-apocalyptic novel.", numberOfPages: "287"],
                [title: "Gone Girl", description: "A dark psychological thriller with shocking twists.", numberOfPages: "422"],
                [title: "The Girl on the Train", description: "A gripping psychological thriller about memory and murder.", numberOfPages: "395"],
                [title: "The Book Thief", description: "A story of hope and loss in Nazi Germany.", numberOfPages: "552"],
                [title: "Life of Pi", description: "An imaginative survival story with spiritual undertones.", numberOfPages: "460"],
                [title: "The Kite Runner", description: "A novel of friendship and redemption set in Afghanistan.", numberOfPages: "371"],
                [title: "The Fault in Our Stars", description: "A touching love story between teenagers with cancer.", numberOfPages: "313"],
                [title: "Memoirs of a Geisha", description: "An immersive journey into Japanese culture and history.", numberOfPages: "434"],
                [title: "The Help", description: "A novel about race, class, and friendship in the American South.", numberOfPages: "464"],
                [title: "The Hunger Games", description: "A dystopian young adult adventure of survival.", numberOfPages: "374"],
                [title: "Catch-22", description: "A satirical novel about the absurdities of war.", numberOfPages: "453"],
                [title: "Pride and Prejudice", description: "A classic romantic novel with wit and insight.", numberOfPages: "279"],
                [title: "Jane Eyre", description: "A gothic novel exploring love and independence.", numberOfPages: "500"],
                [title: "Wuthering Heights", description: "A dark, passionate story of love and revenge.", numberOfPages: "416"],
                [title: "Little Women", description: "A timeless novel about family and womanhood.", numberOfPages: "759"],
                [title: "Beloved", description: "A haunting novel about slavery and memory.", numberOfPages: "324"],
                [title: "One Hundred Years of Solitude", description: "A magical realism epic of a family’s generations.", numberOfPages: "417"],
                [title: "The Handmaid’s Tale", description: "A dystopian novel exploring gender and power.", numberOfPages: "311"],
                [title: "The Shining", description: "A terrifying psychological horror novel.", numberOfPages: "447"],
                [title: "Dracula", description: "The classic gothic horror story of the vampire count.", numberOfPages: "418"],
                [title: "Frankenstein", description: "The original science fiction horror novel.", numberOfPages: "280"],
                [title: "Moby Dick", description: "An epic tale of obsession and revenge at sea.", numberOfPages: "635"],
                [title: "War and Peace", description: "A sweeping historical epic set during Napoleon’s invasion of Russia.", numberOfPages: "1225"],
                [title: "Crime and Punishment", description: "A psychological exploration of guilt and redemption.", numberOfPages: "671"],
                [title: "The Brothers Karamazov", description: "A philosophical novel about faith, doubt, and family.", numberOfPages: "796"],
                [title: "Les Misérables", description: "A story of injustice, redemption, and love in 19th-century France.", numberOfPages: "1463"],
                [title: "Anna Karenina", description: "A tragic novel of love and societal constraints.", numberOfPages: "864"],
                [title: "Madame Bovary", description: "A realist novel about desire and dissatisfaction.", numberOfPages: "329"],
                [title: "The Odyssey", description: "The epic Greek poem of adventure and homecoming.", numberOfPages: "541"],
                [title: "The Iliad", description: "An epic poem about heroism and the Trojan War.", numberOfPages: "683"],
                [title: "A Tale of Two Cities", description: "A historical novel set during the French Revolution.", numberOfPages: "489"],
                [title: "Great Expectations", description: "A coming-of-age story about ambition and love.", numberOfPages: "505"],
                [title: "Oliver Twist", description: "A social novel exposing the plight of orphans in Victorian London.", numberOfPages: "554"],
                [title: "David Copperfield", description: "A semi-autobiographical novel of personal growth.", numberOfPages: "624"],
                [title: "Bleak House", description: "A satirical novel about the flaws of the legal system.", numberOfPages: "768"],
        ]

        List<Author> authors = Author.list()

        booksData.eachWithIndex { data, index ->
            Author author = authors[random.nextInt(authors.size() - 1) + 1]
            Book book = Book.findByTitle(data.title as String)
            if (!book) {
                book = new Book(
                        title: data.title,
                        author: author,
                        description: data.description,
                        numberOfPages: data.numberOfPages as int,
                        listOfBookInstance: []
                )
                book.save(flush: true)
                println "Created book: ${book.title} by ${book.author.firstName} ${book.author.lastName} (${data.numberOfPages} pages) with errors:=${author.errors}"
            } else {
                println "Book: ${book.title} by ${book.author.firstName} ${book.author.lastName} (${data.numberOfPages} pages) already created"
            }
        }
        createDataBookInstances()
    }


    @Transactional
    def createDataBookInstances() {
        List<Integer> instanceCounts = [
                50, 43, 0, 39, 50, 10, 33, 30, 50, 5, 38, 4, 12, 43, 47, 0, 19, 22, 3, 17,
                14, 4, 50, 27, 43, 1, 2, 25, 31, 0, 7, 4, 20, 7, 3, 29, 0, 44, 24, 9,
                44, 8, 0, 31, 28, 13, 15, 20, 6, 17, 26, 0, 40, 27, 13, 0, 15, 10, 32, 4,
                41, 10, 22, 0, 25, 19, 4, 3, 30, 27, 9, 18, 22, 0, 8, 12, 31, 26, 18, 0,
                13, 24, 50, 11, 3, 0, 12, 5, 4, 37, 2, 49, 12, 21, 35, 0, 50, 33, 0, 17,
                44, 10, 33, 30, 50, 5, 38, 4, 12, 43, 47, 0, 10, 22, 3, 17, 14, 4, 50, 27,
                43, 1, 10, 25, 31, 0, 7, 4, 20, 7, 3, 29, 0, 44, 24, 9, 44, 8, 0, 31, 28,
                13, 15, 19, 6, 17, 26
        ]

        List<Book> books = Book.list(sort: 'id')

        books.eachWithIndex { book, idx ->
            if (book.numberOfInstances == 0) {
                int count = instanceCounts[idx]
                (1..count).each { index ->
                    BookInstance instance = new BookInstance(
                            book: book,
                            isActive: true,
                            serialNumber: random.nextInt(100000),
                            isAvailableB: true,
                            borrowHistoryOfBook: []
                    )
                    instance.save(flush: true)
                }
                println "Created ${count} instances for book: ${book.title}"
            } else {
                println "Book instances for book: ${book.title} already created"
            }
        }

        createDataBorrowed()
    }

    @Transactional
    def createDataBorrowed() {
        Role borrowerRole = Role.findByAuthority('ROLE_BORROWER')
        List<UserRole> userRoles = UserRole.findAllByRole(borrowerRole)
        List<User> borrowerUsers = userRoles*.user
        List<BookInstance> bookInstances = BookInstance.list(sort: 'id')
        List<User> someUsers = User.findAll();
        bookInstances.each { instance ->
            if (BorrowedBooks.countByBookInstance(instance) > 0) {
                println "Skipping bookInstance id=${instance.id}, already has borrow records"
                return
            }

            int borrowCount = random.nextInt(31)

            LocalDate currentDate = LocalDate.of(2020, 1, 1).plusDays(random.nextInt(365 * 3))

            (1..borrowCount).each { idx ->
                int duration = 1 + random.nextInt(22)

                Date requestDate = Date.from(currentDate.atStartOfDay(ZoneId.systemDefault()).toInstant())
                Date approvalDate = null

                Date returnDate = null
                ApprovalStatus status
                Boolean isAvailableB = true
                if (idx == borrowCount && borrowCount % 2 == 0) {
                    if (borrowCount % 3 == 0) {
                        status = ApprovalStatus.APPROVED
                        approvalDate = requestDate
                    } else {
                        status = ApprovalStatus.PENDING
                    }
                    isAvailableB = false
                } else {
                    approvalDate = requestDate
                    currentDate = currentDate.plusDays(duration)
                    returnDate = Date.from(currentDate.atStartOfDay(ZoneId.systemDefault()).toInstant())
                    status = ApprovalStatus.APPROVED

                    int gap = 1 + random.nextInt(30)
                    currentDate = currentDate.plusDays(gap)
                }
                User user = borrowerUsers[random.nextInt(borrowerUsers.size())]
                BorrowedBooks borrowed = new BorrowedBooks(
                        bookInstance: instance,
                        user: user,
                        requestDate: requestDate,
                        approvalDate: approvalDate,
                        returnDate: returnDate,
                        statusOfApproval: status,
                )
                instance.isAvailableB = isAvailableB
                borrowed.save(flush: false)
                instance.save(flush: false)
            }
            println "Created ${borrowCount} borrowed entries for bookInstance id=${instance.id}"
        }
        redirect controller: 'sidBooks', action: 'listAuthor'
    }
}