2024/02/26

Well-known URI

well-known URI 是網站上以 /.well-known/ 開始的網頁路徑,例如 https://www.example.com/.well-known/ 這樣的路徑。Well-known URI 是在 RFC 8615 定義,目的是在不同 http server 之間,提供該 http server 相關的一些資訊。

目前最常見的使用方式有兩種,第一種是在申請 http server SSL 憑證的時候,憑證中心會發送一個檔案給申請者,請申請者將該檔案放到 .well-known 的某個路徑下,讓憑證中心可以檢查確認憑證申請者確實有服務該網域的 http server 的所有權。

例如 Let's Encrypt 就是將驗證的檔案放在這個路徑下面 /.well-known/acme-challenge/,這是給 ACME (Automatic Certificate Management Environment) 使用的

第二種是給 Android iOS 手機使用的

  • /.well-known/assetlinks.json 是 Digital Asset Links,可告訴Android browser,要使用什麼 APP 打開

  • /.well-known/apple-app-site-association Universal Links,是 iOS 使用的

GitHub - moul/awesome-well-known: A curated list of well-known URIs, resources, guides and tools (RFC 5785) 這個網頁提供了 .well-known 網址的使用列表

References

Well-known URI - Wikipedia

2024/02/19

Java Date Time API

以往處理日期是用 java.util.Date,以及 java.util.Calendar,但 Date 裡面不存在時區概念,只是 Timestamp 的一個 wrapper,Calendar 也很奇怪的將一月份的數值設定為 0,Java 8 以後可以改用 java.time.* API

java.time 這個 package 裡面有這些 class

  • Instant – represents a point in time (timestamp)
  • LocalDate – represents a date (year, month, day)
  • LocalDateTime – same as LocalDate, but includes time with nanosecond precision
  • OffsetDateTime – same as LocalDateTime, but with time zone offset
  • LocalTime – time with nanosecond precision and without date information
  • ZonedDateTime – same as OffsetDateTime, but includes a time zone ID
  • OffsetLocalTime – same as LocalTime, but with time zone offset
  • MonthDay – month and day, without year or time
  • YearMonth – month and year, without day or time
  • Duration – amount of time represented in seconds, minutes and hours. Has nanosecond precision
  • Period – amount of time represented in days, months and years

有 Timezone 的時間

Date 跟 Instant 的概念不同,處理不同時區的做法不同,但 Instant 的做法比較直覺

        Instant nowInstant = Instant.now();
        ZoneId zoneIdTaipei = ZoneId.of("Asia/Taipei");
        ZoneId zoneIdTokyo = ZoneId.of("Asia/Tokyo");
        ZonedDateTime nowZonedDateTimeTaipei = ZonedDateTime.ofInstant(nowInstant, zoneIdTaipei);
        ZonedDateTime nowZonedDateTimeTokyo = ZonedDateTime.ofInstant(nowInstant, zoneIdTokyo);
        System.out.println("nowZonedDateTimeTaipei="+nowZonedDateTimeTaipei);
        System.out.println("nowZonedDateTimeTokyo="+nowZonedDateTimeTokyo);
        // 跟想像的一樣,將現在的時間放到不同時區,可得到不同時區的正確時間
//        nowZonedDateTimeTaipei=2023-06-14T14:42:44.873547+08:00[Asia/Taipei]
//        nowZonedDateTimeTokyo=2023-06-14T15:42:44.873547+09:00[Asia/Tokyo]

        Date nowDate = new Date();
        System.out.println("nowDate="+nowDate);
        TimeZone tz = TimeZone.getTimeZone("Asia/Tokyo");
        Calendar calendar = Calendar.getInstance(tz);
        calendar.setTime(nowDate);
        Date nowDate2 = calendar.getTime();
        System.out.println("nowDate2="+nowDate2);
        // 因為 Date 沒有時區的概念,是儲存 GMT 1970/1/1 00:00:00 以後所經過的 ms 數值
        // 但列印 Date 時,會自動根據執行程式的時區,轉換為該時區的時間
        // 把現在時間 Date 放到 Tokyo 時區,取回新的 Date 以後,結果兩個 Date 很奇怪的結果是一樣的
//        nowDate=Wed Jun 14 14:42:44 CST 2023
//        nowDate2=Wed Jun 14 14:42:44 CST 2023

        // 要搭配 SimpleDateFormat
        SimpleDateFormat sdfTaipei = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sdfTaipei.setTimeZone(TimeZone.getTimeZone("Asia/Taipei"));
        SimpleDateFormat sdfTokyo = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sdfTokyo.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
        System.out.println();
        System.out.println("nowDate="+nowDate);
        System.out.println("nowDate Taipei="+sdfTaipei.format(nowDate));
        System.out.println("nowDate Tokyo="+sdfTokyo.format(nowDate));
//        nowDate=Wed Jun 14 14:53:19 CST 2023
//        nowDate Taipei=2023-06-14 14:53:19
//        nowDate Tokyo=2023-06-14 15:53:19

java.time 的優點

immutable 且為 thread-safe,所有的 method 回傳的物件都是產生一個新的物件,物件本身的狀態是永久不變的,因此為 thread-safe。java.util.Date 並不是 thread-safe

因為 method 會回傳新的物件,因此可以做 method chaining

ZonedDateTime nextFriday = LocalDateTime.now()
                .plusHours(1)
                .with(TemporalAdjusters.next(DayOfWeek.FRIDAY))
                .atZone(ZoneId.of("Asia/Taipei"));
System.out.println("plus 1hr nextFriday="+nextFriday);

新舊寫法

以下是一些特定工作,新舊 API 的不同寫法

        /* get current timestamp */
        Date now = new Date();
        Instant instant = Instant.now();
        // 帶有時區的現在時間
        ZonedDateTime zonedDateTime = ZonedDateTime.now();

        /* 特定時間 */
        Date sDate = new GregorianCalendar(1990, Calendar.JANUARY, 15).getTime();
// New
        LocalDate sLocalDate = LocalDate.of(1990, Month.JANUARY, 15);

        /* 時間的加減 */
        GregorianCalendar calendar = new GregorianCalendar();
        calendar.add(Calendar.HOUR_OF_DAY, -5);
        Date fiveHoursBeforeOld = calendar.getTime();

        // New
        LocalDateTime fiveHoursBeforeNew = LocalDateTime.now().minusHours(5);

        /* 修改特定欄位 */
        // Old
        GregorianCalendar calc = new GregorianCalendar();
        calc.set(Calendar.MONTH, Calendar.JUNE);
        Date inJuneOld = calc.getTime();

        // New
        LocalDateTime inJuneNew = LocalDateTime.now().withMonth(Month.JUNE.getValue());

        /* Date Time truncating */
        // Old
        Calendar nowCalc = Calendar.getInstance();
        nowCalc.set(Calendar.MINUTE, 0);
        nowCalc.set(Calendar.SECOND, 0);
        nowCalc.set(Calendar.MILLISECOND, 0);
        Date truncatedOld = nowCalc.getTime();

        // New
        LocalTime truncatedNew = LocalTime.now().truncatedTo(ChronoUnit.HOURS);

        /* TimeZone 轉換 */
        // Old
        GregorianCalendar cal2 = new GregorianCalendar();
        cal2.setTimeZone(TimeZone.getTimeZone("Asia/Taipei"));
        Date centralEasternOld = calendar.getTime();

        // New
        ZonedDateTime centralEasternNew = LocalDateTime.now().atZone(ZoneId.of("Asia/Taipei"));

        /* Time difference */
        // Old
        GregorianCalendar calc3 = new GregorianCalendar();
        Date nowdate = new Date();
        calc3.add(Calendar.HOUR, 1);
        Date hourLater = calc3.getTime();
        long elapsed = hourLater.getTime() - nowdate.getTime();

        // New
        LocalDateTime nowLocalDateTime = LocalDateTime.now();
        LocalDateTime hourLaterLocalDateTime = LocalDateTime.now().plusHours(1);
        Duration span = Duration.between(nowLocalDateTime, hourLaterLocalDateTime);

        /* Date formatter, parsing */
        // Old
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        Date nowdate2 = new Date();
        String formattedDateOld = dateFormat.format(nowdate2);
        try {
            Date parsedDateOld = dateFormat.parse(formattedDateOld);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }

        // New
        LocalDate nowLocalDate = LocalDate.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        String formattedDateNew = nowLocalDate.format(formatter);
        LocalDate parsedDateNew = LocalDate.parse(formattedDateNew, formatter);

新舊 classes 之間能夠互相轉換

        // GregorianCalendar 轉為 Instant
        Instant instantFromCalendar = GregorianCalendar.getInstance().toInstant();
        // GregorianCalendar 轉為 ZonedDateTime
        ZonedDateTime zonedDateTimeFromCalendar = new GregorianCalendar().toZonedDateTime();

        // Instant 轉為 Date
        Date dateFromInstant = Date.from(Instant.now());
        // ZonedDateTime 轉為 GregorianCalendar
        GregorianCalendar calendarFromZonedDateTime = GregorianCalendar.from(ZonedDateTime.now());
        // Date 轉為 Instant
        Instant instantFromDate = new Date().toInstant();
        // TimeZone 轉為 ZoneId
        ZoneId zoneIdFromTimeZone = TimeZone.getTimeZone("Asia/Taipei").toZoneId();

References

Set the Time Zone of a Date in Java | Baeldung

Convert Date to LocalDate or LocalDateTime and Back | Baeldung

Migrating to the New Java 8 Date Time API | Baeldung

How to set time zone of a java.util.Date? - Stack Overflow

2024/02/05

在網頁使用 sqlite

SQLite compiled to JavaScript 透過 WASM,可在網頁直接載入 sqlite db,使用 SQL 指令操作資料庫。WebAssembly或稱wasm是一個低階程式語言,讓開發者能運用自己熟悉的程式語言(最初以C/C++作為實作目標)編譯,再藉虛擬機器引擎在瀏覽器內執行。透過 WebAssembly 可以讓一些 C/C++ 開發的函示庫,移動到網頁裡面運作。sql.js 就是用這種方式,讓網頁可以直接使用 sqlite 資料庫。

使用sql.js要先初始化資料庫物件

引用 javascript

<script src='https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.min.js'></script>

接下來有兩種方式初始化資料庫

方法 1: fetch

    async function initdb() {
        let config = {
            locateFile: filename => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.wasm`
        };
        const sqlPromise = initSqlJs(config);
        const dataPromise = fetch("csv/townsnote.db").then(res => res.arrayBuffer());
        const [SQL, buf] = await Promise.all([sqlPromise, dataPromise])
        const sqlitedb = new SQL.Database(new Uint8Array(buf));
        window.sqlitedb = sqlitedb;
    };

    initdb();

方法 2: XMLHttpRequest

    let config = {
        locateFile: filename => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.wasm`
    };
    initSqlJs(config).then(function(SQL){
        const xhr = new XMLHttpRequest();

        xhr.open('GET', 'csv/townsnote.db', true);
        xhr.responseType = 'arraybuffer';

        xhr.onload = e => {
            const uInt8Array = new Uint8Array(xhr.response);
            const db = new SQL.Database(uInt8Array);

            window.sqlitedb = db;

            // const contents = db.exec("SELECT * FROM towns");
            // contents is now [{columns:['col1','col2',...], values:[[first row], [second row], ...]}]
            // console.log("contents=",contents);
        };
        xhr.send();
    });

初始化資料庫後,就可以直接使用資料庫,執行 SQL 查詢指令

let contents = window.sqlitedb.exec("SELECT * FROM towns where id="+id);

以下是載入 sqlite db,執行一個 SQL 查詢的範例

<!DOCTYPE html>
<html lang="zh-tw">
<head>
    <meta charset="utf-8">
    <title>test</title>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.min.js'></script>
    <script>
    async function initdb() {
        let config = {
            locateFile: filename => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.wasm`
        };
        const sqlPromise = initSqlJs(config);
        const dataPromise = fetch("csv/townsnote.db").then(res => res.arrayBuffer());
        const [SQL, buf] = await Promise.all([sqlPromise, dataPromise])
        const sqlitedb = new SQL.Database(new Uint8Array(buf));
        window.sqlitedb = sqlitedb;
    };

    initdb();

    function get_town_by_id() {
        if(!window.sqlitedb) return;
        let id = document.getElementById('id').value;
        let contents = window.sqlitedb.exec("SELECT * FROM towns where id="+id);
        console.log("contents=", contents);

        var jsonArray = JSON.parse(JSON.stringify(contents))
        console.log("jsonArray=", jsonArray);
        document.getElementById('result').value = JSON.stringify(contents);
    };
    </script>

</head>
<body>
    <input type="text" value="9007010" id="id"></input>
    <button onclick="get_town_by_id();">query</button>
    <br/><br/>
    <textarea id="result" rows="20" cols="50"></textarea>
</body>
</html>