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