mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Assignee field in error reporter
This commit is contained in:
@@ -28,6 +28,7 @@ public abstract class AbstractMessage {
|
||||
private SubmittedReportInfo mySubmissionInfo;
|
||||
private String myAdditionalInfo;
|
||||
private Notification myNotification;
|
||||
private Integer myAssigneeId;
|
||||
|
||||
private final Date myDate;
|
||||
|
||||
@@ -90,4 +91,11 @@ public abstract class AbstractMessage {
|
||||
return myDate;
|
||||
}
|
||||
|
||||
public Integer getAssigneeId() {
|
||||
return myAssigneeId;
|
||||
}
|
||||
|
||||
public void setAssigneeId(Integer assigneeId) {
|
||||
myAssigneeId = assigneeId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2000-2012 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 com.intellij.diagnostic;
|
||||
|
||||
public class Developer {
|
||||
public static final Developer NULL = new NullDeveloper();
|
||||
|
||||
private Integer myId;
|
||||
|
||||
private String myName;
|
||||
|
||||
private Developer() {
|
||||
}
|
||||
|
||||
public Developer(int id, String name) {
|
||||
myId = id;
|
||||
myName = name;
|
||||
}
|
||||
|
||||
public Integer getId() {
|
||||
return myId;
|
||||
}
|
||||
|
||||
public String getDisplayText() {
|
||||
return myName;
|
||||
}
|
||||
|
||||
public String getSearchableText() {
|
||||
return myName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%d - %s", myId, myName);
|
||||
}
|
||||
|
||||
private static class NullDeveloper extends Developer {
|
||||
@Override
|
||||
public Integer getId() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayText() {
|
||||
return "<none>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSearchableText() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NullDeveloper";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2000-2012 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 com.intellij.diagnostic;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
class DevelopersLoader {
|
||||
private static final String DEVELOPERS_LIST_URL = "http://ea-engine.labs.intellij.net/data?category=developers";
|
||||
private static final String DATA_CHARSET = "utf-8";
|
||||
|
||||
private DevelopersLoader() {
|
||||
}
|
||||
|
||||
public static Collection<Developer> fetchDevelopers() throws IOException {
|
||||
try {
|
||||
List<Developer> developers = new LinkedList<Developer>();
|
||||
developers.add(Developer.NULL);
|
||||
|
||||
URL url = new URL(DEVELOPERS_LIST_URL);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), DATA_CHARSET));
|
||||
|
||||
try {
|
||||
while (reader.ready()) {
|
||||
String line = reader.readLine();
|
||||
if (line == null) break;
|
||||
int i = line.indexOf('\t');
|
||||
if (i == -1) throw new IOException("Protocol error");
|
||||
int id = Integer.parseInt(line.substring(0, i));
|
||||
String name = line.substring(i + 1);
|
||||
developers.add(new Developer(id, name));
|
||||
}
|
||||
return developers;
|
||||
} finally {
|
||||
reader.close();
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,8 +121,14 @@ public class ITNReporter extends ErrorReportSubmitter {
|
||||
}
|
||||
}
|
||||
|
||||
if (event.getData() instanceof LogMessageEx) {
|
||||
errorBean.setAttachments(((LogMessageEx)event.getData()).getAttachments());
|
||||
Object data = event.getData();
|
||||
|
||||
if (data instanceof AbstractMessage) {
|
||||
errorBean.setAssigneeId(((AbstractMessage)data).getAssigneeId());
|
||||
}
|
||||
|
||||
if (data instanceof LogMessageEx) {
|
||||
errorBean.setAttachments(((LogMessageEx)data).getAttachments());
|
||||
}
|
||||
|
||||
@NonNls String login = errorReportConfigurable.ITN_LOGIN;
|
||||
|
||||
@@ -8,6 +8,10 @@ import com.intellij.ide.impl.DataManagerImpl;
|
||||
import com.intellij.ide.plugins.IdeaPluginDescriptor;
|
||||
import com.intellij.ide.plugins.PluginManager;
|
||||
import com.intellij.ide.util.PropertiesComponent;
|
||||
import com.intellij.notification.Notification;
|
||||
import com.intellij.notification.NotificationDisplayType;
|
||||
import com.intellij.notification.NotificationType;
|
||||
import com.intellij.notification.Notifications;
|
||||
import com.intellij.openapi.actionSystem.*;
|
||||
import com.intellij.openapi.application.Application;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
@@ -22,6 +26,9 @@ import com.intellij.openapi.extensions.ExtensionException;
|
||||
import com.intellij.openapi.extensions.Extensions;
|
||||
import com.intellij.openapi.extensions.PluginDescriptor;
|
||||
import com.intellij.openapi.extensions.PluginId;
|
||||
import com.intellij.openapi.progress.ProgressIndicator;
|
||||
import com.intellij.openapi.progress.ProgressManager;
|
||||
import com.intellij.openapi.progress.Task;
|
||||
import com.intellij.openapi.project.DumbAware;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.ui.DialogWrapper;
|
||||
@@ -42,15 +49,15 @@ import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.util.text.DateFormatUtil;
|
||||
import com.intellij.util.ui.UIUtil;
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import javax.swing.event.HyperlinkEvent;
|
||||
import javax.swing.event.HyperlinkListener;
|
||||
import javax.swing.event.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -63,6 +70,7 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene
|
||||
@NonNls private static final String ACTIVE_TAB_OPTION = IdeErrorsDialog.class.getName() + "activeTab";
|
||||
public static DataKey<String> CURRENT_TRACE_KEY = DataKey.create("current_stack_trace_key");
|
||||
public static final int COMPONENTS_WIDTH = 670;
|
||||
public static Collection<Developer> ourDevelopersList = Collections.emptyList();
|
||||
|
||||
private JPanel myContentPane;
|
||||
private JPanel myBackButtonPanel;
|
||||
@@ -105,6 +113,38 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene
|
||||
}
|
||||
setCancelButtonText(CommonBundle.message("close.action.name"));
|
||||
setModal(false);
|
||||
if (INTERNAL_MODE) {
|
||||
if (ourDevelopersList.isEmpty()) {
|
||||
loadDevelopersAsynchronously();
|
||||
} else {
|
||||
myDetailsTabForm.setDevelopers(ourDevelopersList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadDevelopersAsynchronously() {
|
||||
Task.Backgroundable task = new Task.Backgroundable(null, "Loading developers list", true) {
|
||||
private final Collection[] myDevelopers = new Collection[]{Collections.emptyList()};
|
||||
|
||||
@Override
|
||||
public void run(@NotNull ProgressIndicator indicator) {
|
||||
try {
|
||||
myDevelopers[0] = DevelopersLoader.fetchDevelopers();
|
||||
} catch (IOException e) {
|
||||
Notifications.Bus.register("Error reporter", NotificationDisplayType.BALLOON);
|
||||
Notifications.Bus.notify(new Notification("Error reporter", "Communication error",
|
||||
"Unable to load developers list from server.", NotificationType.WARNING));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
Collection<Developer> developers = myDevelopers[0];
|
||||
myDetailsTabForm.setDevelopers(developers);
|
||||
ourDevelopersList = developers;
|
||||
}
|
||||
};
|
||||
ProgressManager.getInstance().run(task);
|
||||
}
|
||||
|
||||
private boolean moveSelectionToMessage(LogMessage defaultMessage) {
|
||||
@@ -304,6 +344,18 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene
|
||||
}
|
||||
});
|
||||
|
||||
myDetailsTabForm.addAssigneeListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
if (myMute) return;
|
||||
|
||||
AbstractMessage message = getSelectedMessage();
|
||||
if (message != null) {
|
||||
message.setAssigneeId(myDetailsTabForm.getAssigneeId());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return myContentPane;
|
||||
}
|
||||
|
||||
@@ -377,6 +429,7 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene
|
||||
final AbstractMessage message = getSelectedMessage();
|
||||
updateInfoLabel(message);
|
||||
updateCredentialsPane(message);
|
||||
updateAssigneePane(message);
|
||||
updateAttachmentWarning(message);
|
||||
myDisableLink.setVisible(canDisablePlugin(message));
|
||||
updateForeignPluginLabel(message != null ? message : null);
|
||||
@@ -457,6 +510,11 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene
|
||||
myCredentialsPanel.setVisible(false);
|
||||
}
|
||||
|
||||
private void updateAssigneePane(AbstractMessage message) {
|
||||
final ErrorReportSubmitter submitter = getSubmitter(message.getThrowable());
|
||||
myDetailsTabForm.setAssigneeVisible(submitter instanceof ITNReporter && INTERNAL_MODE);
|
||||
}
|
||||
|
||||
private void updateInfoLabel(AbstractMessage message) {
|
||||
if (message == null) {
|
||||
myInfoLabel.setText("");
|
||||
@@ -632,6 +690,8 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene
|
||||
myDetailsTabForm.setCommentsTextEnabled(false);
|
||||
}
|
||||
|
||||
myDetailsTabForm.setAssigneeId(message == null ? null : message.getAssigneeId());
|
||||
|
||||
List<Attachment> attachments =
|
||||
message instanceof LogMessageEx ? ((LogMessageEx)message).getAttachments() : Collections.<Attachment>emptyList();
|
||||
if (!attachments.isEmpty()) {
|
||||
@@ -891,7 +951,12 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene
|
||||
if (logMessage instanceof LogMessageEx) {
|
||||
return ((LogMessageEx)logMessage).toEvent();
|
||||
}
|
||||
return new IdeaLoggingEvent(logMessage.getMessage(), logMessage.getThrowable());
|
||||
return new IdeaLoggingEvent(logMessage.getMessage(), logMessage.getThrowable()) {
|
||||
@Override
|
||||
public AbstractMessage getData() {
|
||||
return logMessage;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.intellij.diagnostic.errordialog.DetailsTabForm">
|
||||
<grid id="27dc6" binding="myContentPane" layout-manager="GridLayoutManager" row-count="3" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<grid id="27dc6" binding="myContentPane" layout-manager="GridLayoutManager" row-count="4" 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="20" y="20" width="589" height="400"/>
|
||||
@@ -52,6 +52,31 @@
|
||||
<text value="Analyze Stacktrace"/>
|
||||
</properties>
|
||||
</component>
|
||||
<grid id="4d3ba" binding="myAssigneePanel" layout-manager="GridLayoutManager" row-count="1" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<margin top="0" left="0" bottom="0" right="0"/>
|
||||
<constraints>
|
||||
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="8" fill="2" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<component id="2ae62" 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>
|
||||
<labelFor value="6901a"/>
|
||||
<text value="&Assignee:"/>
|
||||
</properties>
|
||||
</component>
|
||||
<component id="e5fc4" class="javax.swing.JComboBox" binding="myAssigneeComboBox">
|
||||
<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>
|
||||
</children>
|
||||
</grid>
|
||||
</form>
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
package com.intellij.diagnostic.errordialog;
|
||||
|
||||
import com.intellij.diagnostic.Developer;
|
||||
import com.intellij.diagnostic.DiagnosticBundle;
|
||||
import com.intellij.diagnostic.IdeErrorsDialog;
|
||||
import com.intellij.ui.ComboboxSpeedSearch;
|
||||
import com.intellij.ui.IdeBorderFactory;
|
||||
import com.intellij.util.containers.ComparatorUtil;
|
||||
import com.intellij.util.ui.UIUtil;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* @author ksafonov
|
||||
@@ -18,6 +24,10 @@ public class DetailsTabForm {
|
||||
private LabeledTextComponent myCommentsArea;
|
||||
private JPanel myDetailsHolder;
|
||||
private JButton myAnalyzeStacktraceButton;
|
||||
private JComboBox myAssigneeComboBox;
|
||||
private JPanel myAssigneePanel;
|
||||
private Integer myAssigneeId;
|
||||
private boolean myProcessEvents = true;
|
||||
|
||||
public DetailsTabForm(@Nullable Action analyzeAction) {
|
||||
myCommentsArea.setTitle(DiagnosticBundle.message("error.dialog.comment.prompt"));
|
||||
@@ -30,6 +40,20 @@ public class DetailsTabForm {
|
||||
else {
|
||||
myAnalyzeStacktraceButton.setVisible(false);
|
||||
}
|
||||
myAssigneeComboBox.setRenderer(new DeveloperRenderer(myAssigneeComboBox.getRenderer()));
|
||||
myAssigneeComboBox.setPrototypeDisplayValue(new Developer(0, "Here Goes Some Very Long String"));
|
||||
myAssigneeComboBox.addActionListener(new ActionListenerProxy(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
myAssigneeId = getAssigneeId();
|
||||
}
|
||||
}));
|
||||
new ComboboxSpeedSearch(myAssigneeComboBox) {
|
||||
@Override
|
||||
protected String getElementText(Object element) {
|
||||
return element == null ? "" : ((Developer) element).getSearchableText();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void setCommentsAreaVisible(boolean b) {
|
||||
@@ -65,4 +89,68 @@ public class DetailsTabForm {
|
||||
myCommentsArea.addCommentsListener(l);
|
||||
}
|
||||
|
||||
public void setAssigneeVisible(boolean visible) {
|
||||
myAssigneePanel.setVisible(visible);
|
||||
}
|
||||
|
||||
public void setDevelopers(Collection<Developer> developers) {
|
||||
myAssigneeComboBox.setModel(new DefaultComboBoxModel(developers.toArray()));
|
||||
updateSelectedDeveloper();
|
||||
}
|
||||
|
||||
public void setAssigneeId(@Nullable Integer assigneeId) {
|
||||
myAssigneeId = assigneeId;
|
||||
if (myAssigneeComboBox.getItemCount() > 0) {
|
||||
updateSelectedDeveloper();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSelectedDeveloper() {
|
||||
myProcessEvents = false;
|
||||
|
||||
Integer index = null;
|
||||
for (int i = 0; i < myAssigneeComboBox.getItemCount(); i++) {
|
||||
Developer developer = (Developer) myAssigneeComboBox.getItemAt(i);
|
||||
if (ComparatorUtil.equalsNullable(developer.getId(), myAssigneeId)) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setSelectedAssigneeIndex(index);
|
||||
|
||||
myProcessEvents = true;
|
||||
}
|
||||
|
||||
private void setSelectedAssigneeIndex(Integer index) {
|
||||
if (index == null) {
|
||||
myAssigneeComboBox.setSelectedItem(null);
|
||||
} else {
|
||||
myAssigneeComboBox.setSelectedIndex(index);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getAssigneeId() {
|
||||
Developer assignee = (Developer) myAssigneeComboBox.getSelectedItem();
|
||||
return assignee == null ? null : assignee.getId();
|
||||
}
|
||||
|
||||
public void addAssigneeListener(ActionListener listener) {
|
||||
myAssigneeComboBox.addActionListener(new ActionListenerProxy(listener));
|
||||
}
|
||||
|
||||
private class ActionListenerProxy implements ActionListener {
|
||||
private final ActionListener myDelegate;
|
||||
|
||||
public ActionListenerProxy(ActionListener delegate) {
|
||||
myDelegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
if (myProcessEvents) {
|
||||
myDelegate.actionPerformed(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2000-2012 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 com.intellij.diagnostic.errordialog;
|
||||
|
||||
import com.intellij.diagnostic.Developer;
|
||||
import com.intellij.ide.ui.ListCellRendererWrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
class DeveloperRenderer extends ListCellRendererWrapper {
|
||||
DeveloperRenderer(ListCellRenderer listCellRenderer) {
|
||||
super(listCellRenderer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(JList list, Object value, int index, boolean selected, boolean hasFocus) {
|
||||
setText(value == null ? "<unavailable>" : ((Developer) value).getDisplayText());
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ public class ErrorBean {
|
||||
private String message;
|
||||
private String stackTrace;
|
||||
private String description;
|
||||
private Integer assigneeId;
|
||||
|
||||
private List<Attachment> attachments = Collections.emptyList();
|
||||
|
||||
@@ -107,4 +108,12 @@ public class ErrorBean {
|
||||
public List<Attachment> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public Integer getAssigneeId() {
|
||||
return assigneeId;
|
||||
}
|
||||
|
||||
public void setAssigneeId(Integer assigneeId) {
|
||||
this.assigneeId = assigneeId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,8 @@ public class ITNProxy {
|
||||
|
||||
params.add(Pair.create("error.description", error.getDescription()));
|
||||
|
||||
params.add(Pair.create("assignee.id", error.getAssigneeId() == null ? null : Integer.toString(error.getAssigneeId())));
|
||||
|
||||
for (Attachment attachment : error.getAttachments()) {
|
||||
params.add(Pair.create("attachment.name", attachment.getName()));
|
||||
params.add(Pair.create("attachment.value", attachment.getEncodedBytes()));
|
||||
|
||||
Reference in New Issue
Block a user