This Bugzilla instance is a read-only archive of historic NetBeans bug reports. To report a bug in NetBeans please follow the project's instructions for reporting issues.

View | Details | Raw Unified | Return to bug 71515
Collapse All | Expand All

(-)a/java.api.common/src/org/netbeans/modules/java/api/common/project/BaseActionProvider.java (+2 lines)
Lines 45-50 Link Here
45
package org.netbeans.modules.java.api.common.project;
45
package org.netbeans.modules.java.api.common.project;
46
46
47
import java.awt.Dialog;
47
import java.awt.Dialog;
48
import java.awt.EventQueue;
48
import java.awt.event.MouseEvent;
49
import java.awt.event.MouseEvent;
49
import java.beans.PropertyChangeEvent;
50
import java.beans.PropertyChangeEvent;
50
import java.beans.PropertyChangeListener;
51
import java.beans.PropertyChangeListener;
Lines 384-389 Link Here
384
385
385
    @Override
386
    @Override
386
    public void invokeAction( final String command, final Lookup context ) throws IllegalArgumentException {
387
    public void invokeAction( final String command, final Lookup context ) throws IllegalArgumentException {
388
        assert EventQueue.isDispatchThread();
387
        if (COMMAND_DELETE.equals(command)) {
389
        if (COMMAND_DELETE.equals(command)) {
388
            DefaultProjectOperations.performDefaultDeleteOperation(project);
390
            DefaultProjectOperations.performDefaultDeleteOperation(project);
389
            return ;
391
            return ;
(-)a/projectui/nbproject/project.xml (-1 / +1 lines)
Lines 108-114 Link Here
108
                    <compile-dependency/>
108
                    <compile-dependency/>
109
                    <run-dependency>
109
                    <run-dependency>
110
                        <release-version>1</release-version>
110
                        <release-version>1</release-version>
111
                        <specification-version>1.33</specification-version>
111
                        <specification-version>1.43</specification-version>
112
                    </run-dependency>
112
                    </run-dependency>
113
                </dependency>
113
                </dependency>
114
                <dependency>
114
                <dependency>
(-)a/projectui/src/org/netbeans/modules/project/ui/actions/ActionsUtil.java (-1 / +2 lines)
Lines 48-53 Link Here
48
import java.util.Arrays;
48
import java.util.Arrays;
49
import java.util.HashMap;
49
import java.util.HashMap;
50
import java.util.HashSet;
50
import java.util.HashSet;
51
import java.util.LinkedHashSet;
51
import java.util.List;
52
import java.util.List;
52
import java.util.Map;
53
import java.util.Map;
53
import java.util.Set;
54
import java.util.Set;
Lines 90-96 Link Here
90
         */
91
         */
91
        // #74161: do not cache
92
        // #74161: do not cache
92
        // First find out whether there is a project directly in the Lookup
93
        // First find out whether there is a project directly in the Lookup
93
        Set<Project> result = new HashSet<Project>();
94
        Set<Project> result = new LinkedHashSet<Project>(); // XXX or use OpenProjectList.projectByDisplayName?
94
        for (Project p : lookup.lookupAll(Project.class)) {
95
        for (Project p : lookup.lookupAll(Project.class)) {
95
            result.add(p);
96
            result.add(p);
96
        }
97
        }
(-)a/projectui/src/org/netbeans/modules/project/ui/actions/FileAction.java (+1 lines)
Lines 113-118 Link Here
113
            r[0] = new Runnable() {
113
            r[0] = new Runnable() {
114
                @Override public void run() {
114
                @Override public void run() {
115
                    Project[] projects = ActionsUtil.getProjectsFromLookup( context, command );
115
                    Project[] projects = ActionsUtil.getProjectsFromLookup( context, command );
116
                    // XXX #64991: handle >1 project (tricky since must pass subset of selection to each)
116
                    if ( projects.length != 1 ) {
117
                    if ( projects.length != 1 ) {
117
                        if (projects.length == 0 && globalProvider(context) != null) {
118
                        if (projects.length == 0 && globalProvider(context) != null) {
118
                            enable[0] = true;
119
                            enable[0] = true;
(-)a/projectui/src/org/netbeans/modules/project/ui/actions/MainProjectAction.java (-94 / +56 lines)
Lines 45-66 Link Here
45
package org.netbeans.modules.project.ui.actions;
45
package org.netbeans.modules.project.ui.actions;
46
46
47
import java.awt.Dialog;
47
import java.awt.Dialog;
48
import java.awt.Toolkit;
49
import java.awt.event.MouseEvent;
48
import java.awt.event.MouseEvent;
50
import java.beans.PropertyChangeEvent;
49
import java.beans.PropertyChangeEvent;
51
import java.beans.PropertyChangeListener;
50
import java.beans.PropertyChangeListener;
52
import java.text.MessageFormat;
51
import java.text.MessageFormat;
53
import java.util.Arrays;
52
import java.util.Arrays;
53
import java.util.LinkedList;
54
import javax.swing.Icon;
54
import javax.swing.Icon;
55
import javax.swing.JButton;
55
import javax.swing.JButton;
56
import javax.swing.event.ChangeEvent;
56
import javax.swing.event.ChangeEvent;
57
import javax.swing.event.ChangeListener;
57
import javax.swing.event.ChangeListener;
58
import org.netbeans.api.project.Project;
58
import org.netbeans.api.project.Project;
59
import org.netbeans.api.project.ProjectUtils;
60
import org.netbeans.api.project.ui.OpenProjects;
59
import org.netbeans.api.project.ui.OpenProjects;
61
import org.netbeans.modules.project.ui.NoMainProjectWarning;
60
import org.netbeans.modules.project.ui.NoMainProjectWarning;
62
import org.netbeans.modules.project.ui.OpenProjectList;
61
import org.netbeans.modules.project.ui.OpenProjectList;
63
import org.netbeans.spi.project.ActionProvider;
64
import org.netbeans.spi.project.ui.support.ProjectActionPerformer;
62
import org.netbeans.spi.project.ui.support.ProjectActionPerformer;
65
import org.openide.DialogDescriptor;
63
import org.openide.DialogDescriptor;
66
import org.openide.DialogDisplayer;
64
import org.openide.DialogDisplayer;
Lines 72-80 Link Here
72
import org.openide.util.NbBundle;
70
import org.openide.util.NbBundle;
73
import org.openide.util.WeakListeners;
71
import org.openide.util.WeakListeners;
74
72
75
/** Invokes command on the main project.
73
/**
76
 *
74
 * Similar to {@link ProjectAction} but has a different selection model.
77
 * @author Pet Hrebejk
75
 * First uses the main project, if set.
76
 * Else uses the selected projects, if any.
77
 * Finally, if just one project is open, uses that.
78
 */
78
 */
79
public class MainProjectAction extends LookupSensitiveAction implements PropertyChangeListener {
79
public class MainProjectAction extends LookupSensitiveAction implements PropertyChangeListener {
80
80
Lines 124-181 Link Here
124
124
125
125
126
    public @Override void actionPerformed(Lookup context) {
126
    public @Override void actionPerformed(Lookup context) {
127
        // first try to find main project
127
        Project mainProject = OpenProjectList.getDefault().getMainProject();
128
        Project p = OpenProjectList.getDefault().getMainProject();
128
        Project[] projects = selection(mainProject, context);
129
130
        // then try to find some selected project
131
        if (p == null) {
132
            Project[] projects = ActionsUtil.getProjectsFromLookup(context, command);
133
            if (projects.length == 1) {
134
                p = projects[0];
135
            }
136
        }
137
138
        // then if there is only one project opened in IDE - use it
139
        if (p == null) {
140
            Project[] projects = OpenProjects.getDefault().getOpenProjects();
141
            if (projects.length == 1) {
142
                p = projects[0];
143
            }
144
        }
145
129
146
        // if no main project or no selected or more than one project opened,
130
        // if no main project or no selected or more than one project opened,
147
        // then show warning and allow choose a main project
131
        // then show warning and allow choose a main project
148
        if (p == null) {
132
        if (projects.length == 0) {
149
            // show warning, if cancel then return
133
            // show warning, if cancel then return
150
            if (showNoMainProjectWarning (OpenProjectList.getDefault().getOpenProjects (), 
134
            if (showNoMainProjectWarning (OpenProjectList.getDefault().getOpenProjects (), 
151
                    getPresenterName(name, OpenProjectList.getDefault().getMainProject(), p))) {
135
                    getPresenterName(name, mainProject, projects))) {
152
                return ;
136
                return ;
153
            }
137
            }
154
            p = OpenProjectList.getDefault().getMainProject();
138
            projects = new Project[] {OpenProjectList.getDefault().getMainProject()};
155
        }
139
        }
156
140
157
        if ( command != null ) {
141
        if (command != null && projects.length > 0) {
158
            ActionProvider ap = p.getLookup().lookup(ActionProvider.class);
142
            ProjectAction.runSequentially(new LinkedList<Project>(Arrays.asList(projects)), this, command);
159
            if (ap != null) {
143
        } else if (performer != null && projects.length == 1) {
160
                if (Arrays.asList(ap.getSupportedActions()).contains(command)) {
144
            performer.perform(projects[0]);
161
                    ap.invokeAction(command, Lookup.EMPTY);
162
                } else {
163
                    // #47160: was a supported command (e.g. on a freeform project) but was then removed.
164
                    Toolkit.getDefaultToolkit().beep();
165
                    refreshView(null, false);
166
                }
167
            }
168
        }
169
        else {
170
            performer.perform( p );
171
        }
145
        }
172
    }
146
    }
173
147
174
175
    // Private methods ---------------------------------------------------------
176
177
    // Implementation of PropertyChangeListener --------------------------------
178
179
    public @Override void propertyChange( PropertyChangeEvent evt ) {
148
    public @Override void propertyChange( PropertyChangeEvent evt ) {
180
        if (OpenProjectList.PROPERTY_MAIN_PROJECT.equals(evt.getPropertyName()) ||
149
        if (OpenProjectList.PROPERTY_MAIN_PROJECT.equals(evt.getPropertyName()) ||
181
            OpenProjectList.PROPERTY_OPEN_PROJECTS.equals(evt.getPropertyName())) {
150
            OpenProjectList.PROPERTY_OPEN_PROJECTS.equals(evt.getPropertyName())) {
Lines 183-232 Link Here
183
        }
152
        }
184
    }
153
    }
185
154
155
    private Project[] selection(Project mainProject, Lookup context) {
156
        if (mainProject != null) {
157
            return new Project[] {mainProject};
158
        }
159
        Lookup theContext = context;
160
        if (theContext == null) {
161
            theContext = LastActivatedWindowLookup.INSTANCE;
162
        }
163
        if (theContext != null) {
164
            Project[] projects = ActionsUtil.getProjectsFromLookup(theContext, command);
165
            if (projects.length > 0) {
166
                return projects;
167
            }
168
        }
169
        Project[] projects = OpenProjects.getDefault().getOpenProjects();
170
        if (projects.length == 1) {
171
            return projects;
172
        }
173
        return new Project[0];
174
    }
175
186
    private void refreshView(final Lookup context, boolean immediate) {
176
    private void refreshView(final Lookup context, boolean immediate) {
187
        Runnable r= new Runnable() {
177
        Runnable r= new Runnable() {
188
            public @Override void run() {
178
            public @Override void run() {
189
179
190
        Project p = OpenProjectList.getDefault().getMainProject();
180
        Project mainProject = OpenProjectList.getDefault().getMainProject();
191
        Lookup theContext = context;
181
        Project[] projects = selection(mainProject, context);
192
182
193
        if (p == null) {
183
        final String presenterName = getPresenterName(name, mainProject, projects);
194
            if (theContext == null) {
195
                theContext = LastActivatedWindowLookup.INSTANCE;
196
            }
197
            if (theContext != null) {
198
                Project[] projects = ActionsUtil.getProjectsFromLookup(theContext, command);
199
                if (projects.length == 1) {
200
                    p = projects[0];
201
                }
202
            }
203
        }
204
205
        if (p == null) {
206
            Project[] projects = OpenProjects.getDefault().getOpenProjects();
207
            if (projects.length == 1) {
208
                p = projects[0];
209
            }
210
        }
211
212
        Project mainProject = OpenProjectList.getDefault().getMainProject();
213
214
        final String presenterName = getPresenterName(name, mainProject, p);
215
        final boolean enabled;
184
        final boolean enabled;
216
185
217
        if ( command == null ) {
186
        if ( command == null ) {
218
            enabled = performer.enable(p);
187
            enabled = projects.length == 1 && performer.enable(projects[0]);
219
        }
188
        }
220
        else {
189
        else if (projects.length == 0) {
221
            if ( p == null ) {
190
            enabled = false;
222
                enabled = false;
191
        } else {
192
            boolean e = true;
193
            for (Project p : projects) {
194
                if (!ActionsUtil.commandSupported(p, command, Lookup.EMPTY)) {
195
                    e = false;
196
                    break;
197
                }
223
            }
198
            }
224
            else if ( ActionsUtil.commandSupported ( p, command, Lookup.EMPTY ) ) {
199
            enabled = e;
225
                enabled = true;
226
            }
227
            else {
228
                enabled = false;
229
            }
230
        }
200
        }
231
201
232
        Mutex.EVENT.writeAccess(new Runnable() {
202
        Mutex.EVENT.writeAccess(new Runnable() {
Lines 245-266 Link Here
245
        }
215
        }
246
    }
216
    }
247
217
248
    private String getPresenterName(String name, Project mPrj, Project cPrj) {
218
    private String getPresenterName(String name, Project mPrj, Project[] cPrj) {
249
        String toReturn = "";
219
        if (name == null) {
250
        Object[] formatterArgs;
220
            return "";
251
        if (mPrj == null) {
221
        } else if (mPrj == null) {
252
            if (cPrj == null) {
222
            return ActionsUtil.formatProjectSensitiveName(name, cPrj);
253
                formatterArgs = new Object[] { 0 };
254
            } else {
255
                formatterArgs = new Object[] { 1, ProjectUtils.getInformation(cPrj).getDisplayName() };
256
            }
257
        } else {
223
        } else {
258
            formatterArgs = new Object[] { -1 };
224
            return MessageFormat.format(name, -1);
259
        }
225
        }
260
        if (name != null) {
261
            toReturn = MessageFormat.format(name, formatterArgs);
262
        }
263
        return toReturn;
264
    }
226
    }
265
227
266
    private boolean showNoMainProjectWarning(Project[] projects, String action) {
228
    private boolean showNoMainProjectWarning(Project[] projects, String action) {
(-)a/projectui/src/org/netbeans/modules/project/ui/actions/ProjectAction.java (-20 / +57 lines)
Lines 44-54 Link Here
44
44
45
package org.netbeans.modules.project.ui.actions;
45
package org.netbeans.modules.project.ui.actions;
46
46
47
import java.awt.Toolkit;
48
import java.util.Arrays;
49
import java.util.LinkedList;
50
import java.util.Queue;
51
import java.util.concurrent.atomic.AtomicBoolean;
47
import java.util.logging.Level;
52
import java.util.logging.Level;
48
import java.util.logging.LogRecord;
53
import java.util.logging.LogRecord;
49
import javax.swing.Action;
54
import javax.swing.Action;
50
import javax.swing.Icon;
55
import javax.swing.Icon;
51
import org.netbeans.api.project.Project;
56
import org.netbeans.api.project.Project;
57
import org.netbeans.spi.project.ActionProgress;
52
import org.netbeans.spi.project.ActionProvider;
58
import org.netbeans.spi.project.ActionProvider;
53
import org.netbeans.spi.project.ui.support.ProjectActionPerformer;
59
import org.netbeans.spi.project.ui.support.ProjectActionPerformer;
54
import org.openide.awt.Actions;
60
import org.openide.awt.Actions;
Lines 58-63 Link Here
58
import org.openide.util.Lookup;
64
import org.openide.util.Lookup;
59
import org.openide.util.Mutex;
65
import org.openide.util.Mutex;
60
import org.openide.util.NbBundle;
66
import org.openide.util.NbBundle;
67
import org.openide.util.lookup.Lookups;
61
68
62
/** Action sensitive to current project
69
/** Action sensitive to current project
63
 * 
70
 * 
Lines 118-143 Link Here
118
    @Override
125
    @Override
119
    protected void actionPerformed( Lookup context ) {
126
    protected void actionPerformed( Lookup context ) {
120
        Project[] projects = ActionsUtil.getProjectsFromLookup( context, command );
127
        Project[] projects = ActionsUtil.getProjectsFromLookup( context, command );
121
        
128
        if (command != null && projects.length > 0) {
122
        if ( projects.length == 1 ) {
129
            runSequentially(new LinkedList<Project>(Arrays.asList(projects)), this, command);
123
            if ( command != null ) {
130
        } else if (performer != null && projects.length == 1) {
124
                ActionProvider ap = projects[0].getLookup().lookup(ActionProvider.class);
131
            performer.perform(projects[0]);
125
                LogRecord r = new LogRecord(Level.FINE, "PROJECT_ACTION"); // NOI18N
132
        }
126
                r.setResourceBundle(NbBundle.getBundle(ProjectAction.class));
133
    }
127
                r.setParameters(new Object[] {
134
    static void runSequentially(final Queue<Project> queue, final LookupSensitiveAction a, final String command) {
128
                    getClass().getName(),
135
        Project p = queue.remove();
129
                    projects[0].getClass().getName(),
136
        final ActionProvider ap = p.getLookup().lookup(ActionProvider.class);
130
                    getValue(NAME)
137
        if (ap == null) {
131
                });
138
            return;
132
                r.setLoggerName(UILOG.getName());
139
        }
133
                UILOG.log(r);
140
        if (!Arrays.asList(ap.getSupportedActions()).contains(command)) {
134
                ap.invokeAction( command, Lookup.EMPTY );        
141
            // #47160: was a supported command (e.g. on a freeform project) but was then removed.
142
            Toolkit.getDefaultToolkit().beep();
143
            a.refresh(a.getLookup(), false);
144
            return;
145
        }
146
        LogRecord r = new LogRecord(Level.FINE, "PROJECT_ACTION"); // NOI18N
147
        r.setResourceBundle(NbBundle.getBundle(ProjectAction.class));
148
        r.setParameters(new Object[] {
149
            a.getClass().getName(),
150
            p.getClass().getName(),
151
            a.getValue(NAME)
152
        });
153
        r.setLoggerName(UILOG.getName());
154
        UILOG.log(r);
155
        Mutex.EVENT.writeAccess(new Runnable() {
156
            @Override public void run() {
157
                if (queue.isEmpty()) {
158
                    ap.invokeAction(command, Lookup.EMPTY);
159
                } else {
160
                    final AtomicBoolean started = new AtomicBoolean();
161
                    ap.invokeAction(command, Lookups.singleton(new ActionProgress() {
162
                        @Override protected void started() {
163
                            started.set(true);
164
                        }
165
                        @Override public void finished(boolean success) {
166
                            if (success) { // OK, next...
167
                                runSequentially(queue, a, command);
168
                            } // else build failed, so skip others
169
                        }
170
                    }));
171
                    if (!started.get()) {
172
                        // Did not run action for some reason; try others?
173
                        runSequentially(queue, a, command);
174
                    }
175
                }
135
            }
176
            }
136
            else if ( performer != null ) {
177
        });
137
                performer.perform( projects[0] );
138
            }
139
        }
140
        
141
    }
178
    }
142
    
179
    
143
    @Override
180
    @Override
Lines 147-153 Link Here
147
        Project[] projects = ActionsUtil.getProjectsFromLookup( context, command );
184
        Project[] projects = ActionsUtil.getProjectsFromLookup( context, command );
148
        final boolean enable;
185
        final boolean enable;
149
        if ( command != null ) {
186
        if ( command != null ) {
150
            enable = projects.length == 1;
187
            enable = projects.length > 0;
151
        } else if ( performer != null && projects.length == 1 ) {
188
        } else if ( performer != null && projects.length == 1 ) {
152
            enable = performer.enable(projects[0]);
189
            enable = performer.enable(projects[0]);
153
        } else {
190
        } else {
(-)a/projectui/test/unit/src/org/netbeans/modules/project/ui/actions/ProjectActionTest.java (-2 / +26 lines)
Lines 47-57 Link Here
47
import java.util.ArrayList;
47
import java.util.ArrayList;
48
import java.util.List;
48
import java.util.List;
49
import javax.swing.Action;
49
import javax.swing.Action;
50
import javax.swing.KeyStroke;
51
import org.netbeans.api.project.Project;
50
import org.netbeans.api.project.Project;
52
import org.netbeans.api.project.ProjectManager;
51
import org.netbeans.api.project.ProjectManager;
53
import org.netbeans.junit.MockServices;
52
import org.netbeans.junit.MockServices;
54
import org.netbeans.junit.NbTestCase;
53
import org.netbeans.junit.NbTestCase;
54
import org.netbeans.spi.project.ActionProgress;
55
import org.netbeans.spi.project.ActionProvider;
55
import org.netbeans.spi.project.ActionProvider;
56
import org.netbeans.spi.project.ui.support.ProjectActionPerformer;
56
import org.netbeans.spi.project.ui.support.ProjectActionPerformer;
57
import org.openide.filesystems.FileObject;
57
import org.openide.filesystems.FileObject;
Lines 77-82 Link Here
77
    private DataObject d2_1;
77
    private DataObject d2_1;
78
    private DataObject d2_2;
78
    private DataObject d2_2;
79
    private TestSupport.TestProject project1;
79
    private TestSupport.TestProject project1;
80
    private TestActionProvider tap1;
80
    private TestSupport.TestProject project2;
81
    private TestSupport.TestProject project2;
81
82
82
    @Override protected boolean runInEQ() {
83
    @Override protected boolean runInEQ() {
Lines 98-104 Link Here
98
        d1_2 = DataObject.find(f1_2);
99
        d1_2 = DataObject.find(f1_2);
99
               
100
               
100
        project1 = (TestSupport.TestProject)ProjectManager.getDefault().findProject( p1 );
101
        project1 = (TestSupport.TestProject)ProjectManager.getDefault().findProject( p1 );
101
        project1.setLookup( Lookups.fixed( new Object[] { new TestActionProvider() } ) );  
102
        tap1 = new TestActionProvider();
103
        project1.setLookup(Lookups.singleton(tap1));
102
        
104
        
103
        p2 = TestSupport.createTestProject( workDir, "project2" );
105
        p2 = TestSupport.createTestProject( workDir, "project2" );
104
        f2_1 = p2.createData("f2_1.java");
106
        f2_1 = p2.createData("f2_1.java");
Lines 125-130 Link Here
125
        assertEnablement(action, false);
127
        assertEnablement(action, false);
126
        lookup.change(d1_1, d2_1);
128
        lookup.change(d1_1, d2_1);
127
        assertEnablement(action, false);
129
        assertEnablement(action, false);
130
        TestActionProvider tap2 = new TestActionProvider();
131
        project2.setLookup(Lookups.singleton(tap2));
132
        lookup.change(d2_1);
133
        assertEnablement(action, true);
134
        lookup.change(d1_1, d2_1);
135
        assertEnablement(action, true);
136
        action.actionPerformed(null);
137
        assertEquals("[COMMAND]", tap1.invocations.toString());
138
        assertEquals("[COMMAND]", tap2.invocations.toString());
139
        tap1.listenerSuccess = true;
140
        tap2.listenerSuccess = true;
141
        action.actionPerformed(null);
142
        assertEquals("[COMMAND, COMMAND]", tap1.invocations.toString());
143
        assertEquals("[COMMAND, COMMAND]", tap2.invocations.toString());
144
        tap1.listenerSuccess = false;
145
        action.actionPerformed(null);
146
        assertEquals("[COMMAND, COMMAND, COMMAND]", tap1.invocations.toString());
147
        assertEquals("[COMMAND, COMMAND]", tap2.invocations.toString());
128
    }
148
    }
129
    
149
    
130
    public void testProviderEnablement() throws Exception {
150
    public void testProviderEnablement() throws Exception {
Lines 177-182 Link Here
177
        private String[] ACTIONS = new String[] { COMMAND };
197
        private String[] ACTIONS = new String[] { COMMAND };
178
        
198
        
179
        private List<String> invocations = new ArrayList<String>();
199
        private List<String> invocations = new ArrayList<String>();
200
        Boolean listenerSuccess;
180
        
201
        
181
        public String[] getSupportedActions() {
202
        public String[] getSupportedActions() {
182
            return ACTIONS;
203
            return ACTIONS;
Lines 186-191 Link Here
186
            
207
            
187
            if ( COMMAND.equals( command ) ) {
208
            if ( COMMAND.equals( command ) ) {
188
                invocations.add( command );
209
                invocations.add( command );
210
                if (listenerSuccess != null) {
211
                    ActionProgress.start(context).finished(listenerSuccess);
212
                }
189
            }            
213
            }            
190
            else {
214
            else {
191
                throw new IllegalArgumentException();
215
                throw new IllegalArgumentException();

Return to bug 71515