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