View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript.host.file;
16  
17  import java.io.IOException;
18  import java.nio.charset.Charset;
19  import java.nio.charset.StandardCharsets;
20  import java.nio.charset.UnsupportedCharsetException;
21  import java.util.Base64;
22  import java.util.Locale;
23  
24  import org.apache.commons.io.Charsets;
25  import org.apache.commons.logging.Log;
26  import org.apache.commons.logging.LogFactory;
27  import org.htmlunit.corejs.javascript.Function;
28  import org.htmlunit.corejs.javascript.ScriptableObject;
29  import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBuffer;
30  import org.htmlunit.javascript.JavaScriptEngine;
31  import org.htmlunit.javascript.configuration.JsxClass;
32  import org.htmlunit.javascript.configuration.JsxConstant;
33  import org.htmlunit.javascript.configuration.JsxConstructor;
34  import org.htmlunit.javascript.configuration.JsxFunction;
35  import org.htmlunit.javascript.configuration.JsxGetter;
36  import org.htmlunit.javascript.configuration.JsxSetter;
37  import org.htmlunit.javascript.host.event.Event;
38  import org.htmlunit.javascript.host.event.EventTarget;
39  import org.htmlunit.javascript.host.event.ProgressEvent;
40  import org.htmlunit.protocol.data.DataURLConnection;
41  import org.htmlunit.util.MimeType;
42  import org.htmlunit.util.StringUtils;
43  
44  /**
45   * A JavaScript object for {@code FileReader}.
46   *
47   * @author Ahmed Ashour
48   * @author Ronald Brill
49   * @author Lai Quang Duong
50   */
51  @JsxClass
52  public class FileReader extends EventTarget {
53  
54      private static final Log LOG = LogFactory.getLog(FileReader.class);
55  
56      /** No data has been loaded yet. */
57      @JsxConstant
58      public static final int EMPTY = 0;
59  
60      /** Data is currently being loaded. */
61      @JsxConstant
62      public static final int LOADING = 1;
63  
64      /** The entire read request has been completed. */
65      @JsxConstant
66      public static final int DONE = 2;
67  
68      private int readyState_ = EMPTY;
69      private Object result_;
70  
71      /**
72       * JavaScript constructor.
73       */
74      @Override
75      @JsxConstructor
76      public void jsConstructor() {
77          super.jsConstructor();
78      }
79  
80      /**
81       * Returns the current state of the reading operation.
82       *
83       * @return {@value #EMPTY}, {@value #LOADING}, or {@value #DONE}
84       */
85      @JsxGetter
86      public int getReadyState() {
87          return readyState_;
88      }
89  
90      /**
91       * Returns the file's contents.
92       * @return the file's contents
93       */
94      @JsxGetter
95      public Object getResult() {
96          return result_;
97      }
98  
99      /**
100      * Reads the contents of the specified {@link Blob} or {@link File}.
101      * @param object the {@link Blob} or {@link File} from which to read
102      * @throws IOException if an error occurs
103      */
104     @JsxFunction
105     public void readAsDataURL(final Object object) throws IOException {
106         readyState_ = LOADING;
107 
108         if (!(object instanceof Blob blob)) {
109             throw JavaScriptEngine.typeError(
110                     "FileReader.readAsDataURL: Argument 1 does not implement interface Blob.");
111         }
112 
113         result_ = DataURLConnection.DATA_PREFIX;
114 
115         final byte[] bytes = blob.getBytes();
116         fireEvent(new ProgressEvent(this, Event.TYPE_LOAD_START, true, 0, bytes.length));
117         fireEvent(new ProgressEvent(this, Event.TYPE_PROGRESS, true, bytes.length, bytes.length));
118 
119         final String value = new String(Base64.getEncoder().encode(bytes), StandardCharsets.US_ASCII);
120 
121         String contentType = blob.getType();
122         if (StringUtils.isEmptyOrNull(contentType)) {
123             contentType = MimeType.APPLICATION_OCTET_STREAM;
124         }
125 
126         result_ += contentType + ";base64," + value;
127         readyState_ = DONE;
128 
129         fireEvent(new ProgressEvent(this, Event.TYPE_LOAD, true, bytes.length, bytes.length));
130         fireEvent(new ProgressEvent(this, Event.TYPE_LOAD_END, true, bytes.length, bytes.length));
131     }
132 
133     /**
134      * Reads the contents of the specified {@link Blob} or {@link File}.
135      * @param object the {@link Blob} or {@link File} from which to read
136      */
137     @JsxFunction
138     public void readAsArrayBuffer(final Object object) {
139         readyState_ = LOADING;
140 
141         if (!(object instanceof Blob blob)) {
142             throw JavaScriptEngine.typeError(
143                     "FileReader.readAsArrayBuffer: Argument 1 does not implement interface Blob.");
144         }
145 
146         final byte[] bytes = blob.getBytes();
147         fireEvent(new ProgressEvent(this, Event.TYPE_LOAD_START, true, 0, bytes.length));
148         fireEvent(new ProgressEvent(this, Event.TYPE_PROGRESS, true, bytes.length, bytes.length));
149 
150         final NativeArrayBuffer buffer = new NativeArrayBuffer(bytes.length);
151         System.arraycopy(bytes, 0, buffer.getBuffer(), 0, bytes.length);
152         buffer.setParentScope(getParentScope());
153         buffer.setPrototype(ScriptableObject.getClassPrototype(getParentScope(), buffer.getClassName()));
154 
155         result_ = buffer;
156         readyState_ = DONE;
157 
158         fireEvent(new ProgressEvent(this, Event.TYPE_LOAD, true, bytes.length, bytes.length));
159         fireEvent(new ProgressEvent(this, Event.TYPE_LOAD_END, true, bytes.length, bytes.length));
160     }
161 
162     /**
163      * Reads the contents of the specified {@link Blob} or {@link File}.
164      * When the read operation is complete, the readyState is changed to DONE,
165      * the loaded event is triggered, and the result attribute contains the
166      * contents of the file as a text string.
167      * @param object the {@link Blob} or {@link File} from which to read
168      * @param encoding the encoding
169      */
170     @JsxFunction
171     public void readAsText(final Object object, final Object encoding) {
172         readyState_ = LOADING;
173 
174         if (!(object instanceof Blob blob)) {
175             throw JavaScriptEngine.typeError(
176                     "FileReader.readAsText: Argument 1 does not implement interface Blob.");
177         }
178 
179         Charset charset = StandardCharsets.UTF_8;
180         if (encoding != null && !JavaScriptEngine.isUndefined(encoding)) {
181             final String encAsString = JavaScriptEngine.toString(encoding);
182             if (StringUtils.isNotBlank(encAsString)) {
183                 try {
184                     charset = Charsets.toCharset(encAsString.trim().toLowerCase(Locale.ROOT));
185                 }
186                 catch (final UnsupportedCharsetException e) {
187                     if (LOG.isWarnEnabled()) {
188                         LOG.warn("FileReader readAsText was called with an unsupported encoding '"
189                                     + encoding + "'. Using UTF-8 instead.");
190                     }
191                 }
192             }
193         }
194 
195         final byte[] bytes = blob.getBytes();
196         fireEvent(new ProgressEvent(this, Event.TYPE_LOAD_START, true, 0, bytes.length));
197         fireEvent(new ProgressEvent(this, Event.TYPE_PROGRESS, true, bytes.length, bytes.length));
198 
199         result_ = new String(bytes, charset);
200         readyState_ = DONE;
201 
202         fireEvent(new ProgressEvent(this, Event.TYPE_LOAD, true, bytes.length, bytes.length));
203         fireEvent(new ProgressEvent(this, Event.TYPE_LOAD_END, true, bytes.length, bytes.length));
204     }
205 
206     /**
207      * Returns the {@code onloadstart} event handler for this {@link FileReader}.
208      * @return the {@code onloadstart} event handler for this {@link FileReader}
209      */
210     @JsxGetter
211     public Function getOnloadstart() {
212         return getEventHandler(Event.TYPE_LOAD_START);
213     }
214 
215     /**
216      * Sets the {@code onloadstart} event handler for this {@link FileReader}.
217      * @param onloadstart the {@code onloadstart} event handler for this {@link FileReader}
218      */
219     @JsxSetter
220     public void setOnloadstart(final Object onloadstart) {
221         setEventHandler(Event.TYPE_LOAD_START, onloadstart);
222     }
223 
224     /**
225      * Returns the {@code onprogress} event handler for this {@link FileReader}.
226      * @return the {@code onprogress} event handler for this {@link FileReader}
227      */
228     @JsxGetter
229     public Function getOnprogress() {
230         return getEventHandler(Event.TYPE_PROGRESS);
231     }
232 
233     /**
234      * Sets the {@code onprogress} event handler for this {@link FileReader}.
235      * @param onprogress the {@code onprogress} event handler for this {@link FileReader}
236      */
237     @JsxSetter
238     public void setOnprogress(final Object onprogress) {
239         setEventHandler(Event.TYPE_PROGRESS, onprogress);
240     }
241 
242     /**
243      * Returns the {@code onload} event handler for this {@link FileReader}.
244      * @return the {@code onload} event handler for this {@link FileReader}
245      */
246     @JsxGetter
247     public Function getOnload() {
248         return getEventHandler(Event.TYPE_LOAD);
249     }
250 
251     /**
252      * Sets the {@code onload} event handler for this {@link FileReader}.
253      * @param onload the {@code onload} event handler for this {@link FileReader}
254      */
255     @JsxSetter
256     public void setOnload(final Object onload) {
257         setEventHandler(Event.TYPE_LOAD, onload);
258     }
259 
260     /**
261      * Returns the {@code onloadend} event handler for this {@link FileReader}.
262      * @return the {@code onloadend} event handler for this {@link FileReader}
263      */
264     @JsxGetter
265     public Function getOnloadend() {
266         return getEventHandler(Event.TYPE_LOAD_END);
267     }
268 
269     /**
270      * Sets the {@code onloadend} event handler for this {@link FileReader}.
271      * @param onloadend the {@code onloadend} event handler for this {@link FileReader}
272      */
273     @JsxSetter
274     public void setOnloadend(final Object onloadend) {
275         setEventHandler(Event.TYPE_LOAD_END, onloadend);
276     }
277 
278     /**
279      * Returns the {@code onerror} event handler for this {@link FileReader}.
280      * @return the {@code onerror} event handler for this {@link FileReader}
281      */
282     @JsxGetter
283     public Function getOnerror() {
284         return getEventHandler(Event.TYPE_ERROR);
285     }
286 
287     /**
288      * Sets the {@code onerror} event handler for this {@link FileReader}.
289      * @param onerror the {@code onerror} event handler for this {@link FileReader}
290      */
291     @JsxSetter
292     public void setOnerror(final Object onerror) {
293         setEventHandler(Event.TYPE_ERROR, onerror);
294     }
295 }