001/*
002 * Copyright 2024-2026 Revetware LLC.
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, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet.servlet.jakarta;
018
019import jakarta.servlet.ReadListener;
020import jakarta.servlet.ServletInputStream;
021import org.jspecify.annotations.NonNull;
022
023import javax.annotation.concurrent.NotThreadSafe;
024import java.io.ByteArrayInputStream;
025import java.io.IOException;
026import java.io.InputStream;
027
028import static java.lang.String.format;
029import static java.util.Objects.requireNonNull;
030
031/**
032 * Soklet integration implementation of {@link ServletInputStream}.
033 *
034 * @author <a href="https://www.revetkn.com">Mark Allen</a>
035 */
036@NotThreadSafe
037public final class SokletServletInputStream extends ServletInputStream {
038        @NonNull
039        private final InputStream inputStream;
040        @NonNull
041        private Boolean finished;
042        @NonNull
043        private Boolean closed;
044
045        @NonNull
046        public static SokletServletInputStream fromInputStream(@NonNull InputStream inputStream) {
047                requireNonNull(inputStream);
048                return new SokletServletInputStream(inputStream);
049        }
050
051        private SokletServletInputStream(@NonNull InputStream inputStream) {
052                super();
053                requireNonNull(inputStream);
054
055                this.inputStream = inputStream;
056                this.finished = false;
057                this.closed = false;
058
059                if (inputStream instanceof ByteArrayInputStream && ((ByteArrayInputStream) inputStream).available() == 0)
060                        this.finished = true;
061        }
062
063        @NonNull
064        private InputStream getInputStream() {
065                return this.inputStream;
066        }
067
068        @NonNull
069        private Boolean getFinished() {
070                return this.finished;
071        }
072
073        private void setFinished(@NonNull Boolean finished) {
074                requireNonNull(finished);
075                this.finished = finished;
076        }
077
078        @NonNull
079        private Boolean getClosed() {
080                return this.closed;
081        }
082
083        private void setClosed(@NonNull Boolean closed) {
084                requireNonNull(closed);
085                this.closed = closed;
086        }
087
088        private void ensureOpen() throws IOException {
089                if (getClosed())
090                        throw new IOException("Stream is closed");
091        }
092
093        private void updateFinishedAfterRead(int bytesRead,
094                                                                                                                                                         boolean bytesConsumed) {
095                if (bytesRead == -1) {
096                        setFinished(true);
097                        return;
098                }
099
100                if (bytesConsumed && getInputStream() instanceof ByteArrayInputStream
101                                && ((ByteArrayInputStream) getInputStream()).available() == 0) {
102                        setFinished(true);
103                }
104        }
105
106        // Implementation of ServletInputStream methods below:
107
108        @Override
109        public boolean isFinished() {
110                return getFinished();
111        }
112
113        @Override
114        public boolean isReady() {
115                return !getClosed();
116        }
117
118        @Override
119        public int available() throws IOException {
120                ensureOpen();
121                return getInputStream().available();
122        }
123
124        @Override
125        public void close() throws IOException {
126                if (getClosed())
127                        return;
128
129                try {
130                        super.close();
131                        getInputStream().close();
132                } finally {
133                        setClosed(true);
134                        setFinished(true);
135                }
136        }
137
138        @Override
139        public void setReadListener(@NonNull ReadListener readListener) {
140                requireNonNull(readListener);
141                throw new IllegalStateException(format("%s functionality is not supported", ReadListener.class.getSimpleName()));
142        }
143
144        @Override
145        public int read() throws IOException {
146                ensureOpen();
147                int data = getInputStream().read();
148                updateFinishedAfterRead(data, data != -1);
149
150                return data;
151        }
152
153        @Override
154        public int read(@NonNull byte[] b,
155                                                                        int off,
156                                                                        int len) throws IOException {
157                requireNonNull(b);
158                ensureOpen();
159                int count = getInputStream().read(b, off, len);
160                updateFinishedAfterRead(count, count > 0);
161
162                return count;
163        }
164}