001/*
002 * Copyright (c) 2009 The openGion Project.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
013 * either express or implied. See the License for the specific language
014 * governing permissions and limitations under the License.
015 */
016package org.opengion.hayabusa.servlet;
017
018import java.io.ByteArrayInputStream;
019import java.io.File;
020import java.util.Base64;
021import java.awt.image.BufferedImage;
022import javax.imageio.ImageIO;
023
024import java.time.LocalDateTime;
025import java.time.format.DateTimeFormatter;
026
027import java.io.IOException;
028import jakarta.servlet.ServletException;
029import jakarta.servlet.ServletConfig;
030import jakarta.servlet.http.HttpServlet;
031import jakarta.servlet.http.HttpServletRequest;
032import jakarta.servlet.http.HttpServletResponse;
033// import jakarta.servlet.ServletOutputStream;                                                          // 8.0.0.0 (2021/07/31) Delete
034import jakarta.servlet.annotation.WebServlet;
035import jakarta.servlet.annotation.WebInitParam;
036
037import org.opengion.fukurou.util.StringUtil;
038import org.opengion.hayabusa.common.HybsSystem;
039import org.opengion.hayabusa.common.HybsSystemException;
040
041/**
042 * クライアントからBase64でエンコードして送信された画像ファイルを、
043 * ファイルに変換してセーブするサーブレットです。
044 *
045 * 想定される使い方は、クライアント側で、カメラ等で撮影された映像を
046 * canvasに書き込み、それを、Base64でPOSTする感じです。
047 *
048 * 一般的なサーブレットと同様に、デプロイメント・ディスクリプタ WEB-INF/web.xml に、
049 * servlet 要素と そのマッピング(servlet-mapping)を定義する必要があります。
050 *
051 *     <servlet>
052 *         <servlet-name>imageSave</servlet-name>
053 *         <servlet-class>org.opengion.hayabusa.servlet.ImageSave</servlet-class>
054 *       <init-param>
055 *           <param-name>saveDir</param-name>
056 *           <param-value>jsp/snapshot/</param-value>
057 *       </init-param>
058 *       <init-param>
059 *           <param-name>debug</param-name>
060 *           <param-value>false</param-value>
061 *       </init-param>
062 *     </servlet>
063 *
064 *     <servlet-mapping>
065 *         <servlet-name>imageSave</servlet-name>
066 *         <url-pattern>/jsp/imageSave</url-pattern>
067 *     </servlet-mapping>
068 *
069 * 一般には、http://サーバー:ポート/システムID/jsp/imageSave
070 *    引数;img=イメージファイル
071 *     dir=ディレクトリ  (初期値は、saveDir="jsp/snapshot/")
072 *     file=ファイル名   (初期値は、filePtn="yyyyMMddHHmmssSSS" + ".png"(固定))
073 * 形式のURL でPOSTします。
074 *
075 * @og.rev 7.4.2.1 (2021/05/21) 新規追加
076 * @og.group その他機能
077 *
078 * @version  7.4
079 * @author   Kazuhiko Hasegawa
080 * @since    JDK11,
081 */
082@WebServlet(
083        urlPatterns = "/jsp/imageSave" ,
084        initParams  = {
085                @WebInitParam( name="saveDir" , value="jsp/snapshot/" )
086        }
087)
088public class ImageSave extends HttpServlet {
089        private static final long serialVersionUID = 742120210521L ;
090
091        private final static DateTimeFormatter YMDH = DateTimeFormatter.ofPattern( "yyyyMMddHHmmssSSS" );
092
093        /** セーブディレクトリ */
094        private String  saveDir = "jsp/snapshot/";
095//      private boolean isDebug = false;
096        /** デバッグフラグ */
097        private boolean isDebug ;                                               // 8.0.0.0 (2021/07/31) Avoid using redundant field initializer for ***
098
099        /**
100         * デフォルトコンストラクター
101         *
102         * @og.rev 8.5.3.2 (2023/10/13) JDK21対応。警告: デフォルトのコンストラクタの使用で、コメントが指定されていません
103         */
104        public ImageSave() { super(); }         // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。
105
106        /**
107         * Servlet の 初期値設定を行います。
108         *
109         * WEB-INF/web.xml ファイルで、<servlet> タグ内で初期値設定を行います。
110         *       <init-param>
111         *           <param-name>saveDir</param-name>
112         *           <param-value>jsp/snapshot/</param-value>
113         *       </init-param>
114         *
115         * @param       config  ServletConfigオブジェクト
116         */
117        @Override
118        public void init( final ServletConfig config ) throws ServletException {
119                super.init( config );
120
121                saveDir = StringUtil.nval( config.getInitParameter("saveDir") , saveDir );
122
123                // boolean の StringUtil.nval は厳密チェックするので使わない。
124                isDebug = Boolean.parseBoolean( config.getInitParameter( "debug" ) );
125        }
126
127        /**
128         * GET メソッドが呼ばれたときに実行します。
129         *
130         * 処理は、doPost へ振りなおしています。
131         *
132         * @param       request HttpServletRequestオブジェクト
133         * @param       response        HttpServletResponseオブジェクト
134         *
135         * @og.rev 7.4.2.1 (2021/05/21) 新規追加
136         *
137         * @throws ServletException サーブレット関係のエラーが発生した場合、throw されます。
138         * @throws IOException 入出力エラーが発生したとき
139         */
140        @Override
141        public void doGet( final HttpServletRequest request, final HttpServletResponse response )
142                                                        throws ServletException, IOException {
143                doPost( request,response );
144        }
145
146        /**
147         * POST メソッドが呼ばれたときに実行します。
148         *
149         * @param       request HttpServletRequestオブジェクト
150         * @param       response        HttpServletResponseオブジェクト
151         *
152         * @og.rev 7.4.2.1 (2021/05/21) 新規追加
153         *
154         * @throws ServletException サーブレット関係のエラーが発生した場合、throw されます。
155         * @throws IOException 入出力エラーが発生したとき
156         */
157        @Override
158        public void doPost( final HttpServletRequest request, final HttpServletResponse response )
159                                                        throws ServletException, IOException {
160
161                // boolean の StringUtil.nval は厳密チェックするので使わない。
162                final String debugPrm = request.getParameter( "debug" ) ;
163                final boolean debug = StringUtil.isNull( debugPrm )
164                                                                        ? isDebug                                                               // 未指定なら、初期値を使う
165                                                                        : Boolean.parseBoolean( debugPrm );             // "true"以外はfalse になる。
166
167                String data = request.getParameter( "img" );
168                if( StringUtil.isNotNull( data ) ) {
169                        // Javascriptのcanvas.toDataURL()関数から派生したデータを保存したい場合は、
170                        // 空白をプラスに変換する必要があります。そうしないと、デコードされたデータが破損します。
171                        data = data.replace(' ','+');
172
173                        // 相対パスを絶対パスに変換。ファイルセパレータも正規化されています。
174                        final String dir = HybsSystem.url2dir( StringUtil.nval( request.getParameter( "dir" ),saveDir ) );
175                        if( debug ) { System.out.println( "dir=" + dir ); }
176
177                        // ファイル名が無ければ、現在時刻.png
178                        String file = request.getParameter( "file" );
179                        if( StringUtil.isNull( file ) ) {
180                                final LocalDateTime nowDateTime = LocalDateTime.now();
181                                file = nowDateTime.format( YMDH ) + ".png";
182                        }
183
184                        if( debug ) { System.out.println( "file=" + file ); }
185
186                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 ExceptionAsFlowControl 対応
187                        String errMsg = null;
188
189                        // Base64をデコードしてファイルに戻す。
190                        final byte[] bytes = Base64.getDecoder().decode(data);
191                        try( ByteArrayInputStream input = new ByteArrayInputStream(bytes) ) {
192                                final BufferedImage image = ImageIO.read(input);
193                                final File output = new File( dir, file );
194                                final File parent = output.getParentFile();
195                                // 8.5.4.2 (2024/01/12) PMD 7.0.0 CollapsibleIfStatements
196//                              if( parent != null && !parent.exists() ) {                      // parent は null があり得る。存在しない場合のみ。
197//                                      if( !parent.mkdirs() ) {                // 8.0.0.0 (2021/07/31) spotbugs:例外的戻り値を無視
198                                if( parent != null && !parent.exists() && !parent.mkdirs() ) {  // parent は null があり得る。存在しない場合のみ。
199                                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 ExceptionAsFlowControl 対応
200//                                              final String errMsg = "出力先フォルダが生成できませんでした。" + parent ;
201//                                              throw new HybsSystemException( errMsg );
202                                        errMsg = "出力先フォルダが生成できませんでした。" + parent ;
203//                                      }
204                                }
205                                else {
206                                        ImageIO.write(image, "png", output);
207                                        if( debug ) { System.out.println( "output=" + output.getAbsolutePath() ); }
208                                }
209                        }
210                        catch( final Throwable th ) {
211                                // 8.5.4.2 (2024/01/12) PMD 7.0.0 ExceptionAsFlowControl 対応
212//                              final String errMsg = "Base64 デコード処理が失敗しました。" ;
213//                              throw new HybsSystemException( errMsg,th );
214                                errMsg = "Base64 デコード処理が失敗しました。" + th.getMessage();
215                        }
216
217                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 ExceptionAsFlowControl 対応
218                        if( errMsg != null ) {
219                                throw new HybsSystemException( errMsg );
220                        }
221                }
222        }
223}