2021年6月19日土曜日

適格請求書等保存方式(インボイス制度)と消費税の端数処理

消費税の税額計算は 売上税額-仕入れ税額=納税額

2023年10月以降、この納税額の計算の元になる請求書は適格請求書(インボイス)の保存が必要となる。

2019年10月から消費税が10%に引き上げられる際に、日用品等は8%に据え置かれ複数税率を扱う事業者が発生する。

この軽減税率を扱う事業者に関しては2023年9月までは移行措置として、区分記載請求書等保存方式が認められる。

  • 2019年10月~2023年9月 区分記載請求書等保存方式
  • 2023年10月~      適格請求書等保存方式(インボイス方式)

区分記載請求書等の記載事項

  • 請求書等の発行者の氏名又は名称
  • 取引の年月日
  • 取引の内容(販売した物)
  • 請求書等の受領者の氏名又は名称
  • 軽減税率の対象品目である旨  <<--軽減税率を使用する場合
  • 税率ごとに区分して合計した税込対価の額  <<--軽減税率を使用する場合


2023年10月1日からは、適格請求書等保存方式(インボイス制度)という新たな制度が始まる

適格請求書等の記載事項は、区分記載請求書等に下記項目を加えたもの

  • 登録番号
  • 適用した税率
  • 消費税額


消費税の端数処理

結論から言うと、を四捨五入、切捨てまたは切上げのいずれかの方法により処理しも問題ない。ただし、処理するタイミングには注意が必要。


まず、一定期間の取引をまとめた請求書を発行する場合、1請求書当たり1回とされています。

ただし、納品書と請求書のセットなど複数書類で請求書の記載を満たす場合は、伝票単位で端数処理し、請求書は合計したものを使用しても問題ないとされています。





2021年5月10日月曜日

BootStrapを実装サンプルを用いて検証

KingFisherではデザインテンプレートとしてBootstrapを採用したいと思います。(Twitterさんありがとう!)。

Bootstrapは初めて触るので、どんなものか正直手探りですが、まずはサイト(システム)に導入するまでの検証を行いたいと思います。

今回のタスク

  • Bootstrapのライブラリをダウンロード
  • Java Servlet で動くシステムに適用
  • servletを実行するOSはWindowsとする
それでは始めましょう

まずはBootstrapのライブラリをダウンロードします

 https://getbootstrap.jp/へアクセスします

ダウンロードボタンをクリックします

ダウンロードボタンをクリックします

ダウンロードされたzipの中を見るとcssとjsフォルダあありますので、まずbootstrap.css

そしてbootstrap.jsを作成しているservletの開発環境へ持ち込みます

ログイン画面を実装してみます
1つめのポイントは、headタグでbootstrap.cssを読み込むように設定します
<!-- Bootstrap core CSS -->
<head>
<link rel="stylesheet" href="<%= request.getContextPath() %>/contents/css/bootstrap.css"/>
2つめのポイントは、bodyタグの最後でbootstrap.jsを読み込むように設定します
    <script src="<%= request.getContextPath() %>/js/bootstrap.js"></script>
</body>
完成したjspです
<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<jsp:useBean id="Form0010" scope="request" class="jp.co.ngs.kingfisher.form.Form0010" />
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="NipponGuideSystem,.Co.Ltd.">
<META name="description" content="kingfisher">
<META name="keywords" content="kingfisher,販売管理システム">
<title>Login</title>
<style>

.form-signin-message {
	width: 100%;
}
.form-signin {
	width: 100%;
	max-width: 330px;
	padding: 15px;
	margin: auto;
}


.form-signin .form-floating:focus-within {
	z-index: 2;
}

.form-signin input[type="text"] {
	margin-bottom: -1px;
	border-bottom-right-radius: 0;
	border-bottom-left-radius: 0;
}

.form-signin input[type="password"] {
	margin-bottom: 10px;
	border-top-left-radius: 0;
	border-top-right-radius: 0;
}
</style>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="<%= request.getContextPath() %>/contents/css/bootstrap.css"/>

</head>
<!--  <body class="text-center"> -->
<body class="d-flex flex-column h-100">
	<header class="form-signin-message">
		<div class="container">
			<div class="pgm-message text-danger">
				<ul>
					<c:forEach var="data" items="${Form0010.strMessages}">
						<li><c:out value="${data}" /></li>
					</c:forEach>
				</ul>
			</div>
		</div>
	</header>
	<main class="form-signin">
		<div class="container">
		<form method="post" action="./Ctrl0010">
			<h1 class="h3 mb-3 fw-normal">Login</h1>
			<input type="text" id="inputorgId" class="form-control"
				placeholder="組織ID" required autofocus name="orgId" size="8"
				maxlength="8" value="<c:out value="${Form0010.orgId}"/>">
			<input	type="text" id="inputloginid" class="form-control"
				placeholder="ユーザーID" required name="loginId" size="20"
				maxlength="64" value="<c:out value="${Form0010.loginId}"/>">
			<input type="password" id="inputPassword" class="form-control"
				placeholder="パスワード" required name="loginPassword" size="20"
				maxlength="64" value="<c:out value="${Form0010.loginPassword}"/>">
			<button class="w-100 btn btn-lg btn-primary" type="submit">LOGIN</button>
			<p class="mt-5 mb-3 text-muted">© 2021 NipponGuideSystem</p>
		</form>
		</div>
	</main>

    <script src="<%= request.getContextPath() %>/js/bootstrap.js"></script>
</body>
</html>
実行した結果


bootstrapまだまだVersion5の情報が少ないですが、公式マニュアルが充実していますので
そちらを参考にしながら、開発がすすめられそうです。


2021年4月27日火曜日

O/Rマッピングをどうするか決める

Javaプログラムからデータベースへアクセスし、ObjectとRDBのマッピングをするO/Rマッピング。

いくつかの選択肢がありますが、今回はSQLの記述が可能で、チューニングもしやすいといいう理由からMyBatisを採用します。

今回のタスク

  • MyBatisのライブラリをダウンロード
  • アクセスするデータベースのJDBCドライバを準備します
  • mybatis-config.xmlを準備
  • データアクセスクラスを準備する
  • javaマッピングインターフェースクラスとSQLを記述するxmlを準備する
  • データ構造をJavaオブジェクトで表すentityクラスを準備する
  • servletを実行するOSはWindowsとする
それでは始めましょう

まずはMyBatisのライブラリをダウンロードします

https://github.com/mybatis/mybatis-3/releasesへアクセスします

執筆時の最新版は3.5.7でしたので、ここからmybatis-3.5.7.zipをダウンロードします

zipの中からMyBatisのライブラリであるmybatis-3.5.7.jarを取り出し、クラスパスの通っている場所に格納します

次にアクセスするデータベースのJDBCドライバ準備ですが、こちらは過去の回で紹介したMySQLの「connector/j」使用しますので、ここでは説明を割愛します

ここからMyBatisでO/Rマッピングを実現する手順に入りますが、作成するもの一式を絵で表すとこんな感じ

それでは、順に見ていきましょう
mybatis-config.xmlです
データベースへの接続情報と、この後説明します、マッピングインターフェースクラスのあるパッケージ(場所)を指定します。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

	<!-- DB接続設定 -->

  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/javadev?serverTimezone=JST"/>
        <property name="username" value="username"/>
        <property name="password" value="password"/>
      </dataSource>
    </environment>
  </environments>

  <mappers>
    <!--     Mapperのパッケージを指定 -->
    <package name="db.map" />
  </mappers>

</configuration>


データベースアクセスクラスDbAccessorです。SqlSessionFactoryを生成します。MyBatisのサイトにも下記の記載がありますので、シングルトンパターンで実装してみます。

SqlSessionFactory

生成した SqlSessionFactory は、あなたのアプリケーション実行中はそのまま残しておくべきです。 生成した SqlSessionFactory を破棄したり、再度生成する理由はないはずです。 SqlSessionFactory を再生成しない、というのは所謂ベストプラクティスで、これを行なっていたら「何かおかしいぞ」と考えるべきです。 したがって、SqlSessionFactory に最適なのはアプリケーションスコープ、ということになります。 これを実現する方法はいくつもあります。 最も簡単なのはシングルトンパターンまたはスタティックシングルトンパターンを使う方法です。

MyBatis – MyBatis 3 | スタートガイド から引用


package db;
import java.io.IOException;
import java.io.Reader;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
public class DbAccessor {
	private static SqlSessionFactory factory;
	static {
		try {
			String resource = "db/mybatis-config.xml";
			// シングルトンとして利用
			if (factory == null) {
				Reader reader = Resources.getResourceAsReader(resource);
			    // 読み込んだ設定ファイルからSqlSessionFactoryを生成します
			    factory = new SqlSessionFactoryBuilder().build(reader);
			}
		} catch (IOException e) {
			throw new ExceptionInInitializerError(e);
		}
	}
	public static SqlSessionFactory getSqlSessionInstance() {
		return factory;
	}
}

マッピングインターフェースTable001クラスです。今回はListで複数件取得するメソッドとPKで1件取得。更新系としてinsertするものを準備します。

package db.map;
import java.util.List;
import db.entity.Table001Entity;
public interface Table001 {
	/*
	 * 全件取得
	 */
	List<Table001Entity> selectUsers();
	/*
	 * PKで1件取得
	 */
	Table001Entity selectUsersPrimary(int id);
	/*
	 * Insert
	 */
	int insertUser(Table001Entity table001Entity);
}


マッピングインターフェースTable001クラスに対応するSQLを準備します。ここでSQLを記述します。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="db.map.Table001" >

	<select id="selectUsers" resultType="db.entity.Table001Entity">
		<![CDATA[
		select
			id, name, address
		from
			table001
		]]>
	</select>

	<select id="selectUsersPrimary" resultType="db.entity.Table001Entity">
		<![CDATA[
		select
			id, name, address
		from
			table001
		where
			id = #{id}
		]]>
	</select>

	<insert id="insertUser" parameterType="db.entity.Table001Entity">
		<![CDATA[
		insert into table001
			(id, name, address) 
		values
			(#{id}, #{name}, #{address})
		]]>
	</insert>

</mapper>


データ構造をJavaオブジェクトで表すidとnameとaddresを持つTable001Entityクラスを準備します。ここにSQL結果がマッピング(O/Rマッピング)されます。

package db.entity;
/*
 * エンティティクラス
 */
public class Table001Entity {
	int id;
	String name;
	String address;
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getAddress() {
		return address;
	}
	public void setAddress(String address) {
		this.address = address;
	}
}


プログラムからの呼び出し部分です。PKで検索、テーブル全件取得(これを返却しています)、1件Insertするメソッドをサンプルとして掲載します。

	public List<Table001Entity> search() throws Exception {
		List<Table001Entity> result1;
		try {
			SqlSessionFactory factory = DbAccessor.getSqlSessionInstance();
			try (SqlSession session = factory.openSession()) {
				//テーブルのMapperを取得します
				Table001 map = session.getMapper(Table001.class);
                // table001テーブルを検索します
				result1 = map.selectUsers();
				
                // table001テーブルを検索します
				Table001Entity result2 = map.selectUsersPrimary(1);

				Table001Entity result3 = new Table001Entity();
				result3.setId(7);
				result3.setName("PGM-Ins-Name7");
				result3.setAddress("PGM-Ins-Add7");
				int result4 = map.insertUser(result3);
				session.commit();
			}
		}catch(Exception e){
			throw e;
		}finally {
		}
		return result1;
	}


以上、無事O/Rマッピングも実装、確認できました。

2021年4月21日水曜日

ログインに連続失敗した場合の制御を実装してみた(スレッドセーフ版)

 kingFisherでは、ユーザーIDとパスワードを用いてログイン認証を行いますが、招かざるお客様によるパスワードアタックに対処する必要がありますので、サンプル実装してみました。

前回 サンプル実装しましたが、Java.Util.Mapクラスはスレッドセーフで無いので、クラス変数として使用は注意が必要。ともう1つ、拡張For文の中で要素数を削除してはいけないという問題があったので、改修したものを掲載します。

仕様は、「連続3回以上ログインに失敗した場合、10分間はアクセスが制限される」(前回のまま)

まずはログイン失敗情報を記録するクラスを用意

/**
 * 連続ログイン失敗制限用クラス
 * @author naka
 */
public class LoginGuard {

	//最後にアクセスした日時を数値で保持
	private long lastAccess ;

	//アクセス回数
	private int accessCnt;

	public long getLastAccess() {
		return lastAccess;
	}
	public void setLastAccess(long lastAccess) {
		this.lastAccess = lastAccess;
	}
	public int getAccessCnt() {
		return accessCnt;
	}
	public void setAccessCnt(int accessCnt) {
		this.accessCnt = accessCnt;
	}
}


複数スレッドから共有されるようにservletのクラス変数に(ここはMapではなくスレッドセーフなConcurrentMapを使用)、上記クラスを格納できるようにします

@WebServlet("/S02")
public class Servlet02 extends HttpServlet {
	/*
	 * Login連続失敗検証用変数
	 */
	public static ConcurrentMap<String,LoginGuard> loginGuardMap = new ConcurrentHashMap<String,LoginGuard>();

loginGuardCheckメソッドを作成します。

このメソッドは自分を含め誰かかログインすると指定時間(今回は10分)経過しているログイン失敗情報をクリア。続けてログイン試行回数が指定回数(今回は3回)以内かチェックします。

マルチスレッドでのアクセスを考慮し、Map(loginGuardMap)変数へのアクセスをsynchronized ブロックで囲み排他制御します。これはコストがかかりますが、ログイン処理にアクセスが集中することもあまり多くないのと、安全性を考慮した結果採用しようと思います。

それと、前回拡張Forの中で、Mapの要素を削除していましたが、ここはJava.Util.Setを使用し削除対象リストを作成した直後、Forループ外でremoveALLで一括削除しています。

このメソッドをログイン認証の前に呼び出し、falseが返った場合、「アクセスが制限されています」のエラーメッセージを出してログインを拒否します。


	/**
	 * Login連続失敗検証
	 * @param loginid
	 * @return true:ログイン検証してよい false:制限回数オーバー
	 */
	private boolean loginGuardCheck(String loginid) {
		boolean blnReturn = false;
		String strKey = loginid; //ログインしようとしているユーザーID
		//Mapのクリア準備
		Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("JST"),new Locale("ja","JP"));
		long nowDateTime=0;
		nowDateTime = (cal.get(Calendar.YEAR));
		nowDateTime = nowDateTime *100000000;
		nowDateTime = nowDateTime + ((cal.get(Calendar.MONTH)+1)*1000000);
		nowDateTime = nowDateTime + (cal.get(Calendar.DATE)*10000);
		nowDateTime = nowDateTime + (cal.get(Calendar.HOUR_OF_DAY)*100);
		nowDateTime = nowDateTime + (cal.get(Calendar.MINUTE));
		long chkDateTime=0;
		cal.add(Calendar.MINUTE, -10); //10分前を算出
		chkDateTime = (cal.get(Calendar.YEAR));
		chkDateTime = chkDateTime *100000000;
		chkDateTime = chkDateTime + ((cal.get(Calendar.MONTH)+1)*1000000);
		chkDateTime = chkDateTime + (cal.get(Calendar.DATE)*10000);
		chkDateTime = chkDateTime + (cal.get(Calendar.HOUR_OF_DAY)*100);
		chkDateTime = chkDateTime + (cal.get(Calendar.MINUTE));
		//トランザクション排他
		synchronized (loginGuardMap) {
			//Mapのクリア
			Set<String> removeSet = new HashSet<String> ();
			for (String key : loginGuardMap.keySet()) {
				LoginGuard objGuard = loginGuardMap.get(key);
				if (objGuard.getLastAccess() < chkDateTime) {
					removeSet.add(key);
				}
			}
			if(removeSet.size()>0) {
				loginGuardMap.keySet().removeAll(removeSet);
			}

			//過去制限時間内のアクセスチェック
			LoginGuard objGuard = loginGuardMap.get(strKey);
			if (objGuard == null) {
				//初回アクセス
				blnReturn = true;
				objGuard = new LoginGuard();
				objGuard.setAccessCnt(1);
				objGuard.setLastAccess(nowDateTime);
				loginGuardMap.put(strKey, objGuard);
			} else {
				//2回目以降アクセス
				int accessCnt = objGuard.getAccessCnt();
				if (accessCnt < 3) {
					//制限内回数アクセス
					blnReturn = true;
					objGuard.setAccessCnt(++accessCnt);
					objGuard.setLastAccess(nowDateTime);
					loginGuardMap.replace(strKey, objGuard);
				}
			}
		}
		return blnReturn;
	}

最後にログインが成功したら、Mapからそのユーザー情報を削除するメソッドloginGuardResetを用意し、ログイン認証が成功したら、このメソッドを呼び出します

ここもMap(loginGuardMap)変数へのアクセスをsynchronized ブロックで囲み排他制御します。

	/**
	 * Login連続失敗検証用Mapクリア
	 * @param loginid
	 * @return
	 */
	private void loginGuardReset(String orgid,String loginid) {
		String strKey = loginid; //ログインしようとしているユーザーID
		synchronized (loginGuardMap) {
			loginGuardMap.remove(strKey);
		}
	}

サンプル実装の為、指定時間、指定回数を直接書きましたが、コンスタント定義又はパラメタとして外出ししたほうが良いでしょう。あとカレンダー日付を取得しlongに変換しているところ、冗長なので、メソッド分けた方がすっきりしますかね。時間を見て対応しよう。

今回はちょっとコードの量が多くなりましたが以上です。

2021年4月19日月曜日

ログインに連続失敗した場合の制御を実装してみた

 kingFisherでは、ユーザーIDとパスワードを用いてログイン認証を行いますが、招かざるお客様によるパスワードアタックに対処する必要がありますので、サンプル実装してみました。

この実装概念としては良いですが2つの問題を含んでいます。まず1つ目はJava.Util.Mapクラスはスレッドセーフで無いので、クラス変数として使用は注意が必要です。もう1つは、拡張For文の中で要素数を削除してはいけないです。

(そのままコピペで使用しないでくださいね)こちらにスレッドセーフ版を掲載しました。


仕様は、「連続3回以上ログインに失敗した場合、10分間はアクセスが制限される」

まずはログイン失敗情報を記録するクラスを用意

/**
 * 連続ログイン失敗制限用クラス
 * @author naka
 */
public class LoginGuard {

	//最後にアクセスした日時を数値で保持
	private long lastAccess ;

	//アクセス回数
	private int accessCnt;

	public long getLastAccess() {
		return lastAccess;
	}
	public void setLastAccess(long lastAccess) {
		this.lastAccess = lastAccess;
	}
	public int getAccessCnt() {
		return accessCnt;
	}
	public void setAccessCnt(int accessCnt) {
		this.accessCnt = accessCnt;
	}
}


複数スレッドから共有されるようにservletのクラス変数にMap型を用意し、上記クラスを格納できるようにします

@WebServlet("/S02")
public class Servlet02 extends HttpServlet {
	/*
	 * Login連続失敗検証用変数
	 */
	public static Map<String,LoginGuard> loginGuardMap = new HashMap<String,LoginGuard>();

loginGuardCheckメソッドを作成します。

このメソッドは自分を含め誰かかログインすると指定時間(今回は10分)経過しているログイン失敗情報をクリア。続けてログイン試行回数が指定回数(今回は3回)以内かチェックします。

このメソッドをログイン認証の前に呼び出し、falseが返った場合、「アクセスが制限されています」のエラーメッセージを出してログインを拒否します。


	/**
	 * Login連続失敗検証
	 * @param loginid
	 * @return true:ログイン検証してよい false:制限回数オーバー
	 */
	private boolean loginGuardCheck(String loginid) {
		boolean blnReturn = false;
		String strKey = loginid; //ログインしようとしているユーザーID
		//Mapのクリア準備
		Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("JST"),new Locale("ja","JP"));
		long nowDateTime=0;
		nowDateTime = (cal.get(Calendar.YEAR));
		nowDateTime = nowDateTime *100000000;
		nowDateTime = nowDateTime + ((cal.get(Calendar.MONTH)+1)*1000000);
		nowDateTime = nowDateTime + (cal.get(Calendar.DATE)*10000);
		nowDateTime = nowDateTime + (cal.get(Calendar.HOUR_OF_DAY)*100);
		nowDateTime = nowDateTime + (cal.get(Calendar.MINUTE));
		long chkDateTime=0;
		cal.add(Calendar.MINUTE, -10); //10分前を算出
		chkDateTime = (cal.get(Calendar.YEAR));
		chkDateTime = chkDateTime *100000000;
		chkDateTime = chkDateTime + ((cal.get(Calendar.MONTH)+1)*1000000);
		chkDateTime = chkDateTime + (cal.get(Calendar.DATE)*10000);
		chkDateTime = chkDateTime + (cal.get(Calendar.HOUR_OF_DAY)*100);
		chkDateTime = chkDateTime + (cal.get(Calendar.MINUTE));
		//10分前のログ審失敗情報をMapからクリア
		for (String key : loginGuardMap.keySet()) {
			LoginGuard objGuard = loginGuardMap.get(key);
			if (objGuard.getLastAccess() << chkDateTime) {
				loginGuardMap.remove(key);
			}
		}
		//過去制限時間内のアクセスチェック
		LoginGuard objGuard = loginGuardMap.get(strKey);
		if(objGuard == null) {
			//初回アクセスと判断
			blnReturn = true;
			objGuard = new LoginGuard();
			objGuard.setAccessCnt(1);
			objGuard.setLastAccess(nowDateTime);
			loginGuardMap.put(strKey, objGuard);
		}else {
			//2回目以降アクセスなので、3回以下のアクセスか確認
			int accessCnt = objGuard.getAccessCnt();
			if(accessCnt < 4) {
				//制限内回数アクセス
				blnReturn = true;
				objGuard.setAccessCnt(++accessCnt);
				objGuard.setLastAccess(nowDateTime);
				loginGuardMap.replace(strKey, objGuard);
			}
		}
		return blnReturn;
	}

最後にログインが成功したら、Mapからそのユーザー情報を削除するメソッドloginGuardResetを用意し、ログイン認証が成功したら、このメソッドを呼び出します


	/**
	 * Login連続失敗検証用Mapクリア
	 * @param loginid
	 * @return
	 */
	private void loginGuardReset(String orgid,String loginid) {
		String strKey = loginid; //ログインしようとしているユーザーID
		loginGuardMap.remove(strKey);
	}

サンプル実装の為、指定時間、指定回数を直接書きましたが、コンスタント定義又はパラメタとして外出ししたほうが良いでしょう。

今回はちょっとコードの量が多くなりましたが以上です。

2021年4月11日日曜日

PDFBoxで書き込みパスワードを設定する

 kingFisherでは、Javaで生成したPDFをメール送付又は、ダウンロードできることを考えていますが、受け取り者が編集できてしまうのは具合が悪く、読み取りはフリーですが、更新は不可とするためパスワードを付与してみます。

実現するには、org.apache.pdfbox.pdmodel.encryption.AccessPermissionと、org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicyクラスを使用します。

指定方法は以下


//制限を設定
AccessPermission objAp = new AccessPermission();
objAp.setCanAssembleDocument(false);
objAp.setCanExtractContent(false);
objAp.setCanExtractForAccessibility(false);
objAp.setCanFillInForm(false);
objAp.setCanModify(false);
objAp.setCanModifyAnnotations(false);
objAp.setCanPrint(false);
objAp.setCanPrintDegraded(false);
// ap.setReadOnly();
StandardProtectionPolicy spp = new StandardProtectionPolicy("12345", "", objAp);
spp.setEncryptionKeyLength(128);
document.protect(spp);


具体例を示しますと、まず引数にファイル名を渡し、PDF生成部分を呼び出す部分

File pdfOut = new File("C:\\Work\\testpdf001secure.pdf");
createPdf(pdfOut);

呼び出されるPDF作成部分

	protected void createPdf(File pdfOut) throws IOException{
		// 空のドキュメントオブジェクトを作成します
		try(PDDocument document = new PDDocument()) {
			//制限を設定
			AccessPermission objAp = new AccessPermission();
			objAp.setCanAssembleDocument(false);
			objAp.setCanExtractContent(false);
			objAp.setCanExtractForAccessibility(false);
			objAp.setCanFillInForm(false);
			objAp.setCanModify(false);
			objAp.setCanModifyAnnotations(false);
			objAp.setCanPrint(false);
			objAp.setCanPrintDegraded(false);
            // ap.setReadOnly();
            StandardProtectionPolicy spp = new StandardProtectionPolicy("12345", "", objAp);
            spp.setEncryptionKeyLength(128);
            document.protect(spp);

			// 新しいページのオブジェクトを作成します
			PDRectangle rectangle = PDRectangle.A4;
			PDPage page = new PDPage(rectangle);
			document.addPage(page);
			try (TrueTypeCollection ttcG = new TrueTypeCollection(new File("C:/Windows/Fonts/msgothic.ttc"));
				 TrueTypeCollection ttcM = new TrueTypeCollection(new File("C:/Windows/Fonts/msmincho.ttc"))) {

	            TrueTypeFont ttfG = ttcG.getFontByName("MS-Gothic");
	            PDFont fontG = PDType0Font.load(document, ttfG, true);
	            TrueTypeFont ttfM = ttcM.getFontByName("MS-Mincho");
	            PDFont fontM = PDType0Font.load(document, ttfM, true);

	            try(PDPageContentStream contentStream = new PDPageContentStream(document, page)){
	            	//文字出力
	            	contentStream.beginText();
					contentStream.setFont(fontG, 14);
					contentStream.newLineAtOffset(10, rectangle.getHeight() - 40);
					contentStream.showText( "この文字はフォント MSゴシックです" );
					contentStream.endText();
	            	//文字出力
					contentStream.beginText();
					contentStream.setFont(fontM, 14);
					contentStream.newLineAtOffset(10, rectangle.getHeight() - 70);
					contentStream.showText( "この文字はフォント MS明朝です" );
					contentStream.endText();
	            }
				// ドキュメントを保存します
				document.save(pdfOut);
			}
		}
	}


作成されたPDFを開きプロパティを見てみると、パスワードが設定されているのが分かります



2021年4月8日木曜日

決定した環境でPDFBoxが正常に動くか検証

JavaプログラムからPDFを出力するライブライはいくつかありますが、今回は無償で商用利用可能なライセンスとなっているApache PDFBox(検証時最新は3.0)を検証します。

なおPDFBoxはApache Commons-Logging1.2を必要としますので、こちらも併せてダウンロードします。

今回のタスク

  • Apache PDFBoxのライブラリをダウンロード
  • Apache Commons-Loggingライブラリをダウンロード
  • Java ServletからPDFをブラウザに返却するServletクラスを作成して検証
  • PDFに出力する文字には、フォントMS明朝、MSゴシックを使ってみる
  • servletを実行するOSはWindowsとする
それでは始めましょう

まずはApache PDFBoxのライブラリをダウンロードします

 https://pdfbox.apache.org/へアクセスします

Dowunloadをクリックします

下のほうへスクロールします

pdfbox-app-3.0.0-RC1.jarをクリックします

Apacheのサイトにリダイレクトされるので、ここでHTTPS://downloads.apache.org/をクリックします

下のほうへスクロールしていきます

pdfboxがありました。ここをクリックします

バージョンごとにまとめられています。ここで3.0.0-RC1のフォルダをクリックします

ライブラリにたどり着きました。最低限ひつような「fontbox-xxx.jar」と「pdfbox-xxx.jar」をダウンロードします。

続けて、commons-loggingのダウンロードです https://commons.apache.org/proper/commons-logging/download_logging.cgi

下へスクロールします

Binariesの中、commons-logging-1.2-bin.zipをダウンロードします

zipを解凍し、commons-logging-1.2.jarを取り出します。
最終的に、上記に示す3つのjarを使用しますので、これをビルドパスに通してください。

Java Servletを作成します(完成形は下記)
package sample01;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.fontbox.ttf.TrueTypeCollection;
import org.apache.fontbox.ttf.TrueTypeFont;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;

@WebServlet("/S02")
public class Servlet02 extends HttpServlet {

	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		response.setContentType("application/pdf");
		response.setHeader("Cache-Control","pdfs");
		response.setHeader("Pragma","pdfs");
		try(OutputStream out = response.getOutputStream()){
			createPdf(out);
		}

	}

	protected void createPdf(OutputStream out) throws IOException{
		// 空のドキュメントオブジェクトを作成します
		try(PDDocument document = new PDDocument()) {
			// 新しいページのオブジェクトを作成します
			PDRectangle rectangle = PDRectangle.A4;
			PDPage page = new PDPage(rectangle);
			document.addPage(page);
			try (TrueTypeCollection ttcG = new TrueTypeCollection(new File("C:/Windows/Fonts/msgothic.ttc"));
				 TrueTypeCollection ttcM = new TrueTypeCollection(new File("C:/Windows/Fonts/msmincho.ttc"))) {

	            TrueTypeFont ttfG = ttcG.getFontByName("MS-Gothic");
	            PDFont fontG = PDType0Font.load(document, ttfG, true);
	            TrueTypeFont ttfM = ttcM.getFontByName("MS-Mincho");
	            PDFont fontM = PDType0Font.load(document, ttfM, true);

	            try(PDPageContentStream contentStream = new PDPageContentStream(document, page)){
	            	//文字出力
	            	contentStream.beginText();
					contentStream.setFont(fontG, 14);
					contentStream.newLineAtOffset(10, rectangle.getHeight() - 40);
					contentStream.showText( "この文字はフォント MSゴシックです" );
					contentStream.endText();
	            	//文字出力
					contentStream.beginText();
					contentStream.setFont(fontM, 14);
					contentStream.newLineAtOffset(10, rectangle.getHeight() - 70);
					contentStream.showText( "この文字はフォント MS明朝です" );
					contentStream.endText();
	            }
				// ドキュメントを保存します
				document.save(out);
			}
		}
	}
}

ブラウザから、http://localhost:8080/sample01/S02 へアクセスします
上手く、PDFが表示できました。
PDFBoxは無償で利用できますが、GUIでフォーマットを作成したりといったものはありませんので、座標を計算してコーディングするといったひと手間が必要です。



適格請求書等保存方式(インボイス制度)と消費税の端数処理

消費税の税額計算は 売上税額-仕入れ税額=納税額 2023年10月以降、この納税額の計算の元になる請求書は適格請求書(インボイス)の保存が必要となる。 2019年10月から消費税が10%に引き上げられる際に、日用品等は8%に据え置かれ複数税率を扱う事業者が発生する。 この軽減税率...