Add Base class for configure modules with gradle
This commit is contained in:
@@ -168,4 +168,8 @@ public class ConfigureKotlinInProjectUtils {
|
||||
|
||||
private ConfigureKotlinInProjectUtils() {
|
||||
}
|
||||
|
||||
public static void showInfoNotification(@NotNull String message) {
|
||||
Notifications.Bus.notify(new Notification("Configure Kotlin", "Configure Kotlin", message, NotificationType.INFORMATION));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
* Copyright 2010-2013 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jetbrains.jet.plugin.configuration;
|
||||
|
||||
import com.intellij.codeInsight.CodeInsightUtilCore;
|
||||
import com.intellij.openapi.application.Result;
|
||||
import com.intellij.openapi.command.WriteCommandAction;
|
||||
import com.intellij.openapi.module.Module;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.ui.Messages;
|
||||
import com.intellij.openapi.vfs.VfsUtil;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.openapi.vfs.WritingAccessProvider;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import com.intellij.psi.PsiManager;
|
||||
import com.intellij.psi.codeStyle.CodeStyleManager;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.jet.plugin.framework.ui.ConfigureDialogWithModulesAndVersion;
|
||||
import org.jetbrains.plugins.gradle.util.GradleConstants;
|
||||
import org.jetbrains.plugins.groovy.lang.psi.GroovyFile;
|
||||
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory;
|
||||
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrStatement;
|
||||
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
|
||||
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrApplicationStatement;
|
||||
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
|
||||
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrMethodCallExpression;
|
||||
import org.jetbrains.plugins.groovy.lang.psi.api.util.GrStatementOwner;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jetbrains.jet.plugin.configuration.ConfigureKotlinInProjectUtils.showInfoNotification;
|
||||
|
||||
public abstract class KotlinWithGradleConfigurator implements KotlinProjectConfigurator {
|
||||
private static final String[] KOTLIN_VERSIONS = {"0.6.+", "0.1-SNAPSHOT"};
|
||||
|
||||
protected static final String VERSION_TEMPLATE = "$VERSION$";
|
||||
|
||||
protected static final String CLASSPATH = "classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$ext.kotlin_version\"";
|
||||
protected static final String SNAPSHOT_REPOSITORY = "maven {\nurl 'http://oss.sonatype.org/content/repositories/snapshots'\n}";
|
||||
protected static final String REPOSITORY = "mavenCentral()\n";
|
||||
protected static final String LIBRARY = "compile \"org.jetbrains.kotlin:kotlin-stdlib:$ext.kotlin_version\"";
|
||||
protected static final String SOURCE_SET = "main.java.srcDirs += 'src/main/kotlin'\n";
|
||||
protected static final String VERSION = String.format("ext.kotlin_version = '%s'", VERSION_TEMPLATE);
|
||||
|
||||
@Override
|
||||
public boolean isConfigured(@NotNull Module module) {
|
||||
GroovyFile moduleGradleFile = getBuildGradleFile(module.getProject(), getDefaultPathToBuildGradleFile(module));
|
||||
if (moduleGradleFile != null && isFileConfigured(moduleGradleFile)) {
|
||||
return true;
|
||||
}
|
||||
GroovyFile projectGradleFile = getBuildGradleFile(module.getProject(), getDefaultPathToBuildGradleFile(module.getProject()));
|
||||
return projectGradleFile != null && isFileConfigured(projectGradleFile);
|
||||
}
|
||||
|
||||
private boolean isFileConfigured(GroovyFile projectGradleFile) {
|
||||
String fileText = projectGradleFile.getText();
|
||||
return fileText.contains(getApplyPluginDirective()) && fileText.contains(LIBRARY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(Project project) {
|
||||
List<Module> nonConfiguredModules = ConfigureKotlinInProjectUtils.getNonConfiguredModules(project, this);
|
||||
|
||||
ConfigureDialogWithModulesAndVersion dialog =
|
||||
new ConfigureDialogWithModulesAndVersion(project, nonConfiguredModules, KOTLIN_VERSIONS);
|
||||
|
||||
dialog.show();
|
||||
if (!dialog.isOK()) return;
|
||||
|
||||
for (Module module : dialog.getModulesToConfigure()) {
|
||||
String gradleFilePath = getDefaultPathToBuildGradleFile(module);
|
||||
GroovyFile file = getBuildGradleFile(project, gradleFilePath);
|
||||
if (file != null && canConfigureFile(file)) {
|
||||
changeGradleFile(file, dialog.getKotlinVersion());
|
||||
}
|
||||
else {
|
||||
showErrorMessage(project, "Cannot find build.gradle file for module " + module.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addElements(@NotNull GroovyFile file, @NotNull String version) {
|
||||
GrClosableBlock buildScriptBlock = getBuildScriptBlock(file);
|
||||
addFirstExpressionInBlockIfNeeded(VERSION.replace(VERSION_TEMPLATE, version), buildScriptBlock);
|
||||
|
||||
GrClosableBlock buildScriptRepositoriesBlock = getBuildScriptRepositoriesBlock(file);
|
||||
if (isSnapshot(version)) {
|
||||
addLastExpressionInBlockIfNeeded(SNAPSHOT_REPOSITORY, buildScriptRepositoriesBlock);
|
||||
}
|
||||
else if (!buildScriptRepositoriesBlock.getText().contains(REPOSITORY)) {
|
||||
addLastExpressionInBlockIfNeeded(REPOSITORY, buildScriptRepositoriesBlock);
|
||||
}
|
||||
|
||||
GrClosableBlock buildScriptDependenciesBlock = getBuildScriptDependenciesBlock(file);
|
||||
addLastExpressionInBlockIfNeeded(CLASSPATH, buildScriptDependenciesBlock);
|
||||
|
||||
if (!file.getText().contains(getApplyPluginDirective())) {
|
||||
GrExpression apply = GroovyPsiElementFactory.getInstance(file.getProject()).createExpressionFromText(getApplyPluginDirective());
|
||||
GrApplicationStatement applyStatement = getApplyStatement(file);
|
||||
if (applyStatement != null) {
|
||||
file.addAfter(apply, applyStatement);
|
||||
}
|
||||
else {
|
||||
file.addAfter(apply, buildScriptBlock.getParent());
|
||||
}
|
||||
}
|
||||
|
||||
GrClosableBlock repositoriesBlock = getRepositoriesBlock(file);
|
||||
if (isSnapshot(version)) {
|
||||
addLastExpressionInBlockIfNeeded(SNAPSHOT_REPOSITORY, repositoriesBlock);
|
||||
}
|
||||
else if (!repositoriesBlock.getText().contains(REPOSITORY)) {
|
||||
addLastExpressionInBlockIfNeeded(REPOSITORY, repositoriesBlock);
|
||||
}
|
||||
|
||||
GrClosableBlock dependenciesBlock = getDependenciesBlock(file);
|
||||
addLastExpressionInBlockIfNeeded(LIBRARY, dependenciesBlock);
|
||||
|
||||
addSourceSetsBlock(file);
|
||||
}
|
||||
|
||||
protected abstract String getApplyPluginDirective();
|
||||
|
||||
protected abstract void addSourceSetsBlock(@NotNull GroovyFile file);
|
||||
|
||||
protected static boolean canConfigureFile(@NotNull GroovyFile file) {
|
||||
return WritingAccessProvider.isPotentiallyWritable(file.getVirtualFile(), null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected static GroovyFile getBuildGradleFile(Project project, String path) {
|
||||
VirtualFile file = VfsUtil.findFileByIoFile(new File(path), true);
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
|
||||
if (!(psiFile instanceof GroovyFile)) return null;
|
||||
return (GroovyFile) psiFile;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static String getDefaultPathToBuildGradleFile(@NotNull Project project) {
|
||||
return project.getBasePath() + "/" + GradleConstants.DEFAULT_SCRIPT_NAME;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static String getDefaultPathToBuildGradleFile(@NotNull Module module) {
|
||||
return new File(module.getModuleFilePath()).getParent() + "/" + GradleConstants.DEFAULT_SCRIPT_NAME;
|
||||
}
|
||||
|
||||
protected static boolean isSnapshot(@NotNull String version) {
|
||||
return version.contains("SNAPSHOT");
|
||||
}
|
||||
|
||||
protected void changeGradleFile(@NotNull final GroovyFile groovyFile, @NotNull final String version) {
|
||||
new WriteCommandAction(groovyFile.getProject()) {
|
||||
@Override
|
||||
protected void run(Result result) {
|
||||
addElements(groovyFile, version);
|
||||
|
||||
CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(groovyFile);
|
||||
}
|
||||
}.execute();
|
||||
|
||||
VirtualFile virtualFile = groovyFile.getVirtualFile();
|
||||
if (virtualFile != null) {
|
||||
showInfoNotification(virtualFile.getPath() + " was modified");
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static GrClosableBlock getDependenciesBlock(@NotNull GrStatementOwner file) {
|
||||
return getBlockOrCreate(file, "dependencies");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static GrClosableBlock getSourceSetsBlock(@NotNull GrStatementOwner parent) {
|
||||
return getBlockOrCreate(parent, "sourceSets");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static GrClosableBlock getBuildScriptBlock(@NotNull GrStatementOwner file) {
|
||||
return getBlockOrCreate(file, "buildscript");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static GrClosableBlock getBuildScriptDependenciesBlock(@NotNull GrStatementOwner file) {
|
||||
GrClosableBlock buildScript = getBuildScriptBlock(file);
|
||||
return getBlockOrCreate(buildScript, "dependencies");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static GrClosableBlock getBuildScriptRepositoriesBlock(@NotNull GrStatementOwner file) {
|
||||
GrClosableBlock buildScript = getBuildScriptBlock(file);
|
||||
return getBlockOrCreate(buildScript, "repositories");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static GrClosableBlock getRepositoriesBlock(@NotNull GrStatementOwner file) {
|
||||
return getBlockOrCreate(file, "repositories");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static GrClosableBlock getBlockOrCreate(@NotNull GrStatementOwner parent, @NotNull String name) {
|
||||
GrClosableBlock block = getBlockByName(parent, name);
|
||||
if (block == null) {
|
||||
GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(parent.getProject());
|
||||
GrExpression newBlock = factory.createExpressionFromText(name + "{\n}\n");
|
||||
GrStatement[] statements = parent.getStatements();
|
||||
if (statements.length > 0) {
|
||||
parent.addAfter(newBlock, statements[statements.length - 1]);
|
||||
}
|
||||
else {
|
||||
parent.addAfter(newBlock, parent.getFirstChild());
|
||||
}
|
||||
block = getBlockByName(parent, name);
|
||||
assert block != null : "Block should be non-null because it is created";
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
protected static void addLastExpressionInBlockIfNeeded(@NotNull String text, @NotNull GrClosableBlock block) {
|
||||
addExpressionInBlockIfNeeded(text, block, false);
|
||||
}
|
||||
|
||||
protected static void addFirstExpressionInBlockIfNeeded(@NotNull String text, @NotNull GrClosableBlock block) {
|
||||
addExpressionInBlockIfNeeded(text, block, true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static GrClosableBlock getBlockByName(@NotNull PsiElement parent, @NotNull String name) {
|
||||
GrMethodCallExpression[] allExpressions = PsiTreeUtil.getChildrenOfType(parent, GrMethodCallExpression.class);
|
||||
if (allExpressions != null) {
|
||||
for (GrMethodCallExpression expression : allExpressions) {
|
||||
GrExpression invokedExpression = expression.getInvokedExpression();
|
||||
if (invokedExpression == null) continue;
|
||||
if (expression.getClosureArguments().length == 0) continue;
|
||||
|
||||
String expressionText = invokedExpression.getText();
|
||||
if (expressionText.equals(name)) return expression.getClosureArguments()[0];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void addExpressionInBlockIfNeeded(@NotNull String text, @NotNull GrClosableBlock block, boolean isFirst) {
|
||||
if (block.getText().contains(text)) return;
|
||||
GrExpression newStatement = GroovyPsiElementFactory.getInstance(block.getProject()).createExpressionFromText(text);
|
||||
CodeStyleManager.getInstance(block.getProject()).reformat(newStatement);
|
||||
GrStatement[] statements = block.getStatements();
|
||||
if (!isFirst && statements.length > 0) {
|
||||
GrStatement lastStatement = statements[statements.length - 1];
|
||||
if (lastStatement != null) {
|
||||
block.addAfter(newStatement, lastStatement);
|
||||
}
|
||||
}
|
||||
else {
|
||||
PsiElement firstChild = block.getFirstChild();
|
||||
if (firstChild != null) {
|
||||
block.addAfter(newStatement, firstChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static GrApplicationStatement getApplyStatement(@NotNull GroovyFile file) {
|
||||
GrApplicationStatement[] applyStatement = PsiTreeUtil.getChildrenOfType(file, GrApplicationStatement.class);
|
||||
if (applyStatement == null) return null;
|
||||
for (GrApplicationStatement callExpression : applyStatement) {
|
||||
GrExpression invokedExpression = callExpression.getInvokedExpression();
|
||||
if (invokedExpression != null && invokedExpression.getText().equals("apply")) {
|
||||
return callExpression;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static void showErrorMessage(@NotNull Project project, @Nullable String message) {
|
||||
Messages.showErrorDialog(project,
|
||||
"<html>Couldn't configure kotlin-gradle plugin automatically.<br/>" +
|
||||
(message != null ? message : "") +
|
||||
"See manual installation instructions <a href=\"http://confluence.jetbrains.com/display/Kotlin/Kotlin+Build+Tools#KotlinBuildTools-Gradle\">here</a></html>",
|
||||
"Configure Kotlin-Gradle Plugin");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.jetbrains.jet.plugin.configuration;
|
||||
|
||||
import com.intellij.notification.Notification;
|
||||
import com.intellij.notification.NotificationType;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.module.Module;
|
||||
import com.intellij.openapi.project.Project;
|
||||
@@ -24,7 +22,7 @@ import org.jetbrains.jet.plugin.framework.ui.FileUIUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static com.intellij.notification.Notifications.Bus;
|
||||
import static org.jetbrains.jet.plugin.configuration.ConfigureKotlinInProjectUtils.showInfoNotification;
|
||||
|
||||
public abstract class KotlinWithLibraryConfigurator implements KotlinProjectConfigurator {
|
||||
|
||||
@@ -268,10 +266,6 @@ public abstract class KotlinWithLibraryConfigurator implements KotlinProjectConf
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static void showInfoNotification(@NotNull String message) {
|
||||
Bus.notify(new Notification("Configure Kotlin", "Configure Kotlin", message, NotificationType.INFORMATION));
|
||||
}
|
||||
|
||||
protected boolean needToChooseJarPath(@NotNull Project project) {
|
||||
String defaultPath = getDefaultPathToJarFile(project);
|
||||
return !isProjectLibraryPresent(project) && !isJarPresent(defaultPath);
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.jet.plugin.framework.ui.ConfigureDialogWithModulesAndVersion">
|
||||
<grid id="cbd77" binding="contentPane" layout-manager="GridLayoutManager" row-count="3" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<margin top="0" left="0" bottom="0" right="0"/>
|
||||
<constraints>
|
||||
<xy x="48" y="54" width="436" height="297"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<vspacer id="bb37c">
|
||||
<constraints>
|
||||
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
</vspacer>
|
||||
<grid id="46726" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<margin top="10" left="0" bottom="0" right="0"/>
|
||||
<constraints>
|
||||
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<component id="f6ae3" class="javax.swing.JLabel">
|
||||
<constraints>
|
||||
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties>
|
||||
<text value="Choose Kotlin plugin version"/>
|
||||
</properties>
|
||||
</component>
|
||||
<component id="1034b" class="javax.swing.JComboBox" binding="kotlinVersionComboBox">
|
||||
<constraints>
|
||||
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
</component>
|
||||
</children>
|
||||
</grid>
|
||||
<grid id="f8666" binding="chooseModulesPanelPlace" layout-manager="BorderLayout" hgap="0" vgap="0">
|
||||
<constraints>
|
||||
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
<children/>
|
||||
</grid>
|
||||
</children>
|
||||
</grid>
|
||||
<buttonGroups>
|
||||
<group name="sourceGroup">
|
||||
<member id="730af"/>
|
||||
<member id="c709f"/>
|
||||
</group>
|
||||
</buttonGroups>
|
||||
</form>
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2010-2013 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jetbrains.jet.plugin.framework.ui;
|
||||
|
||||
import com.intellij.openapi.module.Module;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.ui.DialogWrapper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.util.List;
|
||||
|
||||
public class ConfigureDialogWithModulesAndVersion extends DialogWrapper {
|
||||
private final ChooseModulePanel chooseModulePanel;
|
||||
|
||||
private JPanel contentPane;
|
||||
private JPanel chooseModulesPanelPlace;
|
||||
private JComboBox kotlinVersionComboBox;
|
||||
|
||||
public ConfigureDialogWithModulesAndVersion(
|
||||
@NotNull Project project,
|
||||
@NotNull List<Module> modules,
|
||||
@NotNull String[] kotlinVersions
|
||||
) {
|
||||
super(project);
|
||||
|
||||
setTitle("Configure Kotlin in Project");
|
||||
|
||||
init();
|
||||
|
||||
chooseModulePanel = new ChooseModulePanel(project, modules);
|
||||
chooseModulesPanelPlace.add(chooseModulePanel.getContentPane(), BorderLayout.CENTER);
|
||||
|
||||
chooseModulePanel.showSeparator();
|
||||
|
||||
for (String version : kotlinVersions) {
|
||||
kotlinVersionComboBox.addItem(version);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Module> getModulesToConfigure() {
|
||||
return chooseModulePanel.getModulesToConfigure();
|
||||
}
|
||||
|
||||
public String getKotlinVersion() {
|
||||
return (String) kotlinVersionComboBox.getSelectedItem();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected JComponent createCenterPanel() {
|
||||
return contentPane;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user