mirror of https://github.com/halo-dev/halo
Cache current theme in theme repository (#1286)
parent
29466c5c76
commit
65ed8b84af
|
@ -1,6 +1,8 @@
|
||||||
package run.halo.app.repository;
|
package run.halo.app.repository;
|
||||||
|
|
||||||
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
|
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
|
||||||
|
import static run.halo.app.model.properties.PrimaryProperties.THEME;
|
||||||
|
import static run.halo.app.model.support.HaloConst.DEFAULT_THEME_ID;
|
||||||
import static run.halo.app.utils.FileUtils.copyFolder;
|
import static run.halo.app.utils.FileUtils.copyFolder;
|
||||||
import static run.halo.app.utils.FileUtils.deleteFolderQuietly;
|
import static run.halo.app.utils.FileUtils.deleteFolderQuietly;
|
||||||
import static run.halo.app.utils.VersionUtil.compareVersion;
|
import static run.halo.app.utils.VersionUtil.compareVersion;
|
||||||
|
@ -14,6 +16,7 @@ import java.util.Optional;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
@ -25,7 +28,6 @@ import run.halo.app.exception.ServiceException;
|
||||||
import run.halo.app.exception.ThemeNotSupportException;
|
import run.halo.app.exception.ThemeNotSupportException;
|
||||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||||
import run.halo.app.model.entity.Option;
|
import run.halo.app.model.entity.Option;
|
||||||
import run.halo.app.model.properties.PrimaryProperties;
|
|
||||||
import run.halo.app.model.support.HaloConst;
|
import run.halo.app.model.support.HaloConst;
|
||||||
import run.halo.app.theme.ThemePropertyScanner;
|
import run.halo.app.theme.ThemePropertyScanner;
|
||||||
import run.halo.app.utils.FileUtils;
|
import run.halo.app.utils.FileUtils;
|
||||||
|
@ -37,7 +39,8 @@ import run.halo.app.utils.FileUtils;
|
||||||
*/
|
*/
|
||||||
@Repository
|
@Repository
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ThemeRepositoryImpl implements ThemeRepository {
|
public class ThemeRepositoryImpl
|
||||||
|
implements ThemeRepository, ApplicationListener<OptionUpdatedEvent> {
|
||||||
|
|
||||||
private final OptionRepository optionRepository;
|
private final OptionRepository optionRepository;
|
||||||
|
|
||||||
|
@ -45,6 +48,8 @@ public class ThemeRepositoryImpl implements ThemeRepository {
|
||||||
|
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
private volatile ThemeProperty currentTheme;
|
||||||
|
|
||||||
public ThemeRepositoryImpl(OptionRepository optionRepository,
|
public ThemeRepositoryImpl(OptionRepository optionRepository,
|
||||||
HaloProperties properties,
|
HaloProperties properties,
|
||||||
ApplicationEventPublisher eventPublisher) {
|
ApplicationEventPublisher eventPublisher) {
|
||||||
|
@ -55,19 +60,35 @@ public class ThemeRepositoryImpl implements ThemeRepository {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getActivatedThemeId() {
|
public String getActivatedThemeId() {
|
||||||
return optionRepository.findByKey(PrimaryProperties.THEME.getValue())
|
return getActivatedThemeProperty().getId();
|
||||||
.map(Option::getValue)
|
|
||||||
.orElse(HaloConst.DEFAULT_THEME_ID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ThemeProperty getActivatedThemeProperty() {
|
public ThemeProperty getActivatedThemeProperty() {
|
||||||
return fetchThemePropertyByThemeId(getActivatedThemeId()).orElseThrow();
|
ThemeProperty themeProperty = this.currentTheme;
|
||||||
|
if (themeProperty == null) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (this.currentTheme == null) {
|
||||||
|
// get current theme id
|
||||||
|
String currentThemeId = this.optionRepository.findByKey(THEME.getValue())
|
||||||
|
.map(Option::getValue)
|
||||||
|
.orElse(DEFAULT_THEME_ID);
|
||||||
|
// fetch current theme
|
||||||
|
this.currentTheme = this.getThemeByThemeId(currentThemeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.currentTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<ThemeProperty> fetchThemePropertyByThemeId(String themeId) {
|
public Optional<ThemeProperty> fetchThemePropertyByThemeId(String themeId) {
|
||||||
return listAll().stream()
|
if (StringUtils.equals(themeId, getActivatedThemeId())) {
|
||||||
|
return Optional.of(getActivatedThemeProperty());
|
||||||
|
}
|
||||||
|
|
||||||
|
return ThemePropertyScanner.INSTANCE.scan(getThemeRootPath(), null)
|
||||||
|
.stream()
|
||||||
.filter(property -> Objects.equals(themeId, property.getId()))
|
.filter(property -> Objects.equals(themeId, property.getId()))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
}
|
}
|
||||||
|
@ -80,13 +101,13 @@ public class ThemeRepositoryImpl implements ThemeRepository {
|
||||||
@Override
|
@Override
|
||||||
public void setActivatedTheme(@NonNull String themeId) {
|
public void setActivatedTheme(@NonNull String themeId) {
|
||||||
Assert.hasText(themeId, "Theme id must not be blank");
|
Assert.hasText(themeId, "Theme id must not be blank");
|
||||||
final var newThemeOption = optionRepository.findByKey(PrimaryProperties.THEME.getValue())
|
final var newThemeOption = optionRepository.findByKey(THEME.getValue())
|
||||||
.map(themeOption -> {
|
.map(themeOption -> {
|
||||||
// set theme id
|
// set theme id
|
||||||
themeOption.setValue(themeId);
|
themeOption.setValue(themeId);
|
||||||
return themeOption;
|
return themeOption;
|
||||||
})
|
})
|
||||||
.orElseGet(() -> new Option(PrimaryProperties.THEME.getValue(), themeId));
|
.orElseGet(() -> new Option(THEME.getValue(), themeId));
|
||||||
optionRepository.save(newThemeOption);
|
optionRepository.save(newThemeOption);
|
||||||
|
|
||||||
eventPublisher.publishEvent(new OptionUpdatedEvent(this));
|
eventPublisher.publishEvent(new OptionUpdatedEvent(this));
|
||||||
|
@ -163,4 +184,20 @@ public class ThemeRepositoryImpl implements ThemeRepository {
|
||||||
private Path getThemeRootPath() {
|
private Path getThemeRootPath() {
|
||||||
return Paths.get(properties.getWorkDir()).resolve("templates/themes");
|
return Paths.get(properties.getWorkDir()).resolve("templates/themes");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplicationEvent(OptionUpdatedEvent event) {
|
||||||
|
synchronized (this) {
|
||||||
|
this.currentTheme = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
protected ThemeProperty getThemeByThemeId(String themeId) {
|
||||||
|
return ThemePropertyScanner.INSTANCE.scan(getThemeRootPath(), null)
|
||||||
|
.stream()
|
||||||
|
.filter(property -> Objects.equals(themeId, property.getId()))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
package run.halo.app.repository;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static run.halo.app.model.properties.PrimaryProperties.THEME;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.mockito.Spy;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import run.halo.app.config.properties.HaloProperties;
|
||||||
|
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||||
|
import run.halo.app.model.support.HaloConst;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme repository impl test.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
class ThemeRepositoryImplTest {
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
@Spy
|
||||||
|
ThemeRepositoryImpl themeRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
OptionRepository optionRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
HaloProperties haloProperties;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
MockitoAnnotations.openMocks(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getActivatedThemeBySingleThread() {
|
||||||
|
ThemeProperty expectedTheme = new ThemeProperty();
|
||||||
|
expectedTheme.setId(HaloConst.DEFAULT_THEME_ID);
|
||||||
|
expectedTheme.setActivated(true);
|
||||||
|
|
||||||
|
given(optionRepository.findByKey(THEME.getValue())).willReturn(Optional.empty());
|
||||||
|
doReturn(expectedTheme).when(themeRepository)
|
||||||
|
.getThemeByThemeId(HaloConst.DEFAULT_THEME_ID);
|
||||||
|
|
||||||
|
ThemeProperty resultTheme = themeRepository.getActivatedThemeProperty();
|
||||||
|
assertEquals(expectedTheme, resultTheme);
|
||||||
|
|
||||||
|
verify(optionRepository, times(1)).findByKey(any());
|
||||||
|
verify(themeRepository, times(1)).getThemeByThemeId(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getActivatedThemeByMultiThread() throws InterruptedException {
|
||||||
|
ThemeProperty expectedTheme = new ThemeProperty();
|
||||||
|
expectedTheme.setId(HaloConst.DEFAULT_THEME_ID);
|
||||||
|
expectedTheme.setActivated(true);
|
||||||
|
|
||||||
|
given(optionRepository.findByKey(THEME.getValue())).willReturn(Optional.empty());
|
||||||
|
doReturn(expectedTheme).when(themeRepository)
|
||||||
|
.getThemeByThemeId(HaloConst.DEFAULT_THEME_ID);
|
||||||
|
|
||||||
|
ExecutorService executorService = Executors.newFixedThreadPool(10);
|
||||||
|
// define tasks
|
||||||
|
List<Callable<ThemeProperty>> tasks = IntStream.range(0, 10)
|
||||||
|
.mapToObj(
|
||||||
|
i -> (Callable<ThemeProperty>) () -> themeRepository.getActivatedThemeProperty())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// invoke and get results
|
||||||
|
executorService.invokeAll(tasks).forEach(future -> {
|
||||||
|
try {
|
||||||
|
assertEquals(expectedTheme, future.get(100, TimeUnit.MILLISECONDS));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to get task result!", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
verify(optionRepository, times(1)).findByKey(any());
|
||||||
|
verify(themeRepository, times(1)).getThemeByThemeId(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue