001/*
002 * Copyright (C) 2022 - 2024, the original author or authors.
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 */
016package io.github.ascopes.jct.assertions;
017
018import static io.github.ascopes.jct.utils.IoExceptionUtils.uncheckedIo;
019import static java.util.Objects.requireNonNull;
020import static org.assertj.core.api.Assertions.assertThat;
021
022import java.io.ByteArrayOutputStream;
023import java.io.UncheckedIOException;
024import java.nio.ByteBuffer;
025import java.nio.charset.Charset;
026import java.nio.charset.CharsetDecoder;
027import java.nio.charset.StandardCharsets;
028import java.time.Instant;
029import javax.tools.JavaFileObject;
030import org.apiguardian.api.API;
031import org.apiguardian.api.API.Status;
032import org.assertj.core.api.AbstractAssert;
033import org.assertj.core.api.AbstractByteArrayAssert;
034import org.assertj.core.api.AbstractInstantAssert;
035import org.assertj.core.api.AbstractStringAssert;
036import org.assertj.core.api.AbstractUriAssert;
037import org.jspecify.annotations.Nullable;
038
039/**
040 * Abstract assertions for {@link JavaFileObject Java file objects}.
041 *
042 * @param <I> the implementation class that is extending this class.
043 * @param <A> the file object implementation type.
044 * @author Ashley Scopes
045 * @since 0.0.1
046 */
047@API(since = "0.0.1", status = Status.STABLE)
048public abstract class AbstractJavaFileObjectAssert<I extends AbstractJavaFileObjectAssert<I, A>, A extends JavaFileObject>
049    extends AbstractAssert<I, A> {
050
051  /**
052   * Initialize this assertion.
053   *
054   * @param actual   the actual value to assert on.
055   * @param selfType the type of the assertion implementation.
056   */
057  @SuppressWarnings("DataFlowIssue")
058  protected AbstractJavaFileObjectAssert(@Nullable A actual, Class<?> selfType) {
059    super(actual, selfType);
060  }
061
062  /**
063   * Get an assertion object on the URI of the file.
064   *
065   * @return the URI assertion.
066   * @throws AssertionError if the actual value is null.
067   */
068  public AbstractUriAssert<?> uri() {
069    isNotNull();
070    return assertThat(actual.toUri());
071  }
072
073  /**
074   * Get an assertion object on the name of the file.
075   *
076   * @return the string assertion.
077   * @throws AssertionError if the actual value is null.
078   */
079  public AbstractStringAssert<?> name() {
080    isNotNull();
081    return assertThat(actual.getName());
082  }
083
084  /**
085   * Get an assertion object on the binary content of the file.
086   *
087   * @return the byte array assertion.
088   * @throws AssertionError if the actual value is null.
089   */
090  public AbstractByteArrayAssert<?> binaryContent() {
091    isNotNull();
092    return assertThat(rawContent());
093  }
094
095  /**
096   * Get an assertion object on the content of the file, using {@link StandardCharsets#UTF_8 UTF-8}
097   * encoding.
098   *
099   * @return the string assertion.
100   * @throws AssertionError       if the actual value is null.
101   * @throws UncheckedIOException if an IO error occurs reading the file content.
102   */
103  public AbstractStringAssert<?> content() {
104    return content(StandardCharsets.UTF_8);
105  }
106
107  /**
108   * Get an assertion object on the content of the file.
109   *
110   * @param charset the charset to decode the file with.
111   * @return the string assertion.
112   * @throws AssertionError       if the actual value is null.
113   * @throws NullPointerException if the charset parameter is null.
114   * @throws UncheckedIOException if an IO error occurs reading the file content.
115   */
116  public AbstractStringAssert<?> content(Charset charset) {
117    requireNonNull(charset, "charset must not be null");
118    return content(charset.newDecoder());
119  }
120
121  /**
122   * Get an assertion object on the content of the file.
123   *
124   * @param charsetDecoder the charset decoder to use to decode the file to a string.
125   * @return the string assertion.
126   * @throws AssertionError       if the actual value is null.
127   * @throws NullPointerException if the charset decoder parameter is null.
128   * @throws UncheckedIOException if an IO error occurs reading the file content.
129   */
130  public AbstractStringAssert<?> content(CharsetDecoder charsetDecoder) {
131    requireNonNull(charsetDecoder, "charsetDecoder must not be null");
132    isNotNull();
133
134    var content = uncheckedIo(() -> charsetDecoder
135        .decode(ByteBuffer.wrap(rawContent()))
136        .toString());
137
138    return assertThat(content);
139  }
140
141  /**
142   * Get an assertion object on the last modified timestamp.
143   *
144   * <p>This will be set to the UNIX epoch ({@code 1970-01-01T00:00:00.000Z}) if an
145   * error occurs reading the file modification time, or if the information is not available.
146   *
147   * @return the instant assertion.
148   * @throws AssertionError if the actual value is null.
149   */
150  public AbstractInstantAssert<?> lastModified() {
151    isNotNull();
152
153    var instant = Instant.ofEpochMilli(actual.getLastModified());
154    return assertThat(instant);
155  }
156
157  /**
158   * Perform an assertion on the file object kind.
159   *
160   * @return the assertions for the kind.
161   * @throws AssertionError if the actual value is null.
162   */
163  public JavaFileObjectKindAssert kind() {
164    isNotNull();
165
166    return new JavaFileObjectKindAssert(actual.getKind());
167  }
168
169  private byte[] rawContent() {
170    return uncheckedIo(() -> {
171      var outputStream = new ByteArrayOutputStream();
172      try (var inputStream = actual.openInputStream()) {
173        inputStream.transferTo(outputStream);
174        return outputStream.toByteArray();
175      }
176    });
177  }
178}